--- url: 'https://docs-deserve.neabyte.com/middleware/basic-auth.md' description: Protect routes with HTTP Basic Authentication middleware in Deserve. --- # Basic Auth Middleware > **Reference**: [MDN HTTP Authentication Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication) HTTP Basic Authentication middleware protects routes with username and password credentials, and stays simple and secure to configure. ## Basic Usage Protect routes with Basic Auth using `Mware.basicAuth()`: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Guard routes with a user list router.use( Mware.basicAuth({ users: [ { username: 'admin', password: 'secret' }, { username: 'user', password: 'pass' } ] }) ) await router.serve(8000) ``` ## Route-Specific Protection Apply Basic Auth only to specific routes: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Protect only /api routes router.use( '/api', Mware.basicAuth({ users: [ { username: 'admin', password: 'secret' } ] }) ) // Protect admin routes with different credentials router.use( '/admin', Mware.basicAuth({ users: [ { username: 'admin', password: 'admin123' } ] }) ) ``` ## Multiple Users Support multiple user accounts: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.use( Mware.basicAuth({ users: [ { username: 'admin', password: 'admin123' }, { username: 'user', password: 'user123' }, { username: 'guest', password: 'guest123' } ] }) ) ``` ## Error Handling A failed login returns **401 Unauthorized** with a `WWW-Authenticate: Basic realm="Secure Area"` header, which is what makes browsers show the login prompt. Credentials are checked in constant time to avoid timing leaks, and an empty `users` array throws `Deno.errors.InvalidData` when the middleware is created. To shape the 401 response, register a single handler with [`router.catch()`](/error-handling/object-details), or rely on the [default behavior](/error-handling/default-behavior). ## Browser Authentication Browsers prompt for credentials automatically when a protected route is accessed: ``` Username: admin Password: ****** ``` --- --- url: 'https://docs-deserve.neabyte.com/static-file/basic.md' description: Serve static files from a directory with the Deserve static handler. --- # Basic Static Serving Serve static files (HTML, CSS, JS, images) using the `static()` method. ## Basic Usage Serve static files from a directory: ![Calling router.static with the prefix slash static and path dot slash public registers the pattern slash static slash star star, then each request has its slash static prefix sliced off ctx.pathname and the remainder joined under public, so slash static maps to public slash index dot html, slash static slash css slash style dot css maps to public slash css slash style dot css, and any segment starting with a dot or dot dot or a path escaping the base is rejected with 404 before any read](/diagrams/static-url-to-file.png) ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // Serve ./public under the /static path router.static('/static', { path: './public', etag: true, cacheControl: 86400 }) await router.serve(8000) ``` This serves files from the `public/` directory at the `/static` URL path: * `GET /static/index.html` → serves `public/index.html` * `GET /static/css/style.css` → serves `public/css/style.css` * `GET /static/js/app.js` → serves `public/js/app.js` ## How It Works Deserve uses a custom static file serving implementation: 1. **Route Matching**: Creates routes with pattern `${urlPath}/**` to match all files 2. **Path Extraction**: reads `ctx.pathname` directly to get the full request path, since FastRouter's `/**` pattern only captures the first segment 3. **File Resolution**: Maps URL paths to file system paths using the `path` option 4. **Priority**: Static routes are registered for all HTTP methods before dynamic routes ### Wildcard Pattern Behavior When `urlPath` is `/`, Deserve creates a `/**` pattern. For path resolution, Deserve uses `ctx.pathname` instead of relying on wildcard parameter, because: * FastRouter's `/**` pattern only captures the **first segment** of the request path instead of the full path (e.g., `"styles"` for `/styles/ui.css`) * To serve nested files correctly, Deserve extracts the full path from `ctx.pathname` and removes the leading `/` to get the relative file path **Example:** * Request: `GET /styles/ui.css` * Pattern: `/**` matches from configurable path * File path: Extracted from `ctx.pathname` → `"styles/ui.css"` * Resolved: `static/styles/ui.css` ## Static File Options The `static()` method accepts a `ServeOptions` object: ### `path` File system directory path to serve files from: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.static('/static', { path: './public' // Serve files from public/ directory }) router.static('/assets', { path: '/absolute/path/to/assets' // Absolute path also supported }) ``` ### `etag` Enable ETag generation for caching. The tag is a SHA-256 hash of the file size and modification time, not the full file content, so it stays cheap to compute: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.static('/static', { path: './public', etag: true // Generate ETag from size and mtime }) ``` When enabled, a client that sends a matching `If-None-Match` header receives a `304 Not Modified` response with no body. ### `cacheControl` Set the Cache-Control max-age in seconds. Deserve sends it as `public, max-age=`, applied only when the value is `0` or higher: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.static('/static', { path: './public', cacheControl: 86400 // Cache for 1 day (86400 seconds) }) router.static('/assets', { path: './assets', cacheControl: 31536000 // Cache for 1 year }) ``` ## File Resolution and Security Static serving maps a URL path to a file under the configured directory, with a few built-in rules: * **Index fallback** - a request to the route root serves `index.html` from the directory. * **Content type** - the type is picked from the file extension. Common web assets like HTML, CSS, JavaScript, JSON, images, fonts, and documents are mapped out of the box, and an unknown extension falls back to `application/octet-stream`. * **Dotfiles blocked** - any path segment whose name starts with `.` is rejected with **404**, so files like `.env`, `.git/config`, or a leading `..` never get served. The rule looks at the segment name, not the extension, so a normal file such as `report.env` is still served. * **Directory traversal blocked** - the resolved real path must stay inside the base directory. A path that escapes it, such as one built from `..`, is rejected with **404**. A missing or blocked file returns 404 through the [centralized error handler](/error-handling/object-details). ## Troubleshooting ### Files Not Found * Check `path` is correct (relative to current working directory or absolute) * Verify file permissions * Ensure files exist in the directory * Check that the URL path matches the route pattern (`/static/file.css` for `router.static('/static', ...)`) ### 404 Errors * Verify the static route is registered before calling `router.serve()` * Check that file paths match the URL structure * Ensure the file exists at the resolved path ### Caching Issues * Verify `etag` and `cacheControl` are set correctly * Check browser DevTools Network tab for ETag and Cache-Control headers * Clear browser cache for testing * Use `304 Not Modified` responses (visible when ETag matches) --- --- url: 'https://docs-deserve.neabyte.com/middleware/body-limit.md' description: Limit incoming request body size to guard against oversized payloads. --- # Body Limit Middleware > **Reference**: [RFC 7230 HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1) Body Limit middleware enforces a maximum request body size. When a body is present, the body stream is always wrapped with a limiter so the size is enforced regardless of headers, which keeps large payloads from overwhelming the server. ## Basic Usage Apply body limit middleware using Deserve's built-in middleware: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Cap request bodies at 1MB router.use( Mware.bodyLimit({ limit: 1024 * 1024 }) ) await router.serve(8000) ``` ## Route-Specific Limits Apply different body limits to specific routes: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // 1MB limit for general routes router.use(Mware.bodyLimit({ limit: 1024 * 1024 })) // 5MB limit for upload routes router.use('/uploads', Mware.bodyLimit({ limit: 5 * 1024 * 1024 })) // 10MB limit for API routes router.use('/api', Mware.bodyLimit({ limit: 10 * 1024 * 1024 })) ``` ## Configuration Options ### `limit` Maximum body size in bytes: ```typescript // 1MB (1,048,576 bytes) limit: 1024 * 1024 // 5MB (5,242,880 bytes) limit: 5 * 1024 * 1024 // 10MB (10,485,760 bytes) limit: 10 * 1024 * 1024 ``` ## How It Works When a request has a body, the middleware wraps the body stream with a byte limiter so the size is enforced as the body is read (not only via headers): 1. **GET/HEAD or no body** - nothing is wrapped and the request passes through. 2. **Body present** - the body stream is wrapped with the limiter. When the client sends more bytes than `limit`, reading stops and the middleware responds with **413**. 3. **Content-Length** - when present without `Transfer-Encoding` and above `limit`, the request is rejected before the body is read. ### RFC 7230 * When both `Transfer-Encoding` and `Content-Length` are present, `Transfer-Encoding` takes precedence. * Chunked or unknown-length bodies are still limited by the wrapped stream, and only the bytes read count toward the limit. ## Complete Example ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Global 1MB limit router.use(Mware.bodyLimit({ limit: 1024 * 1024 })) // Larger limits for uploads and API router.use('/uploads', Mware.bodyLimit({ limit: 5 * 1024 * 1024 })) router.use('/api', Mware.bodyLimit({ limit: 10 * 1024 * 1024 })) await router.serve(8000) ``` ## Error Handling When the limit is exceeded, the middleware returns message `Request body exceeds bytes limit` with **status code 413**. A known `Content-Length` above the limit is rejected before the body is read, while a chunked or oversized stream is rejected as soon as the extra bytes arrive. To shape that response, register a single handler with [`router.catch()`](/error-handling/object-details), or rely on the [default behavior](/error-handling/default-behavior). --- --- url: 'https://docs-deserve.neabyte.com/getting-started/built-for-teams.md' description: >- Patterns for organizing larger Deserve apps with middleware scoping and shared configuration for teams. --- # Built for Teams Deserve keeps the structure of an app obvious, so a team reads the folder tree and already knows the API. There is no central route table to study and no framework-specific wiring to learn first. This follows the [philosophy](/core-concepts/philosophy#core-beliefs) that file structure is the API structure, which keeps a codebase easy for teams to maintain. ## The Folder Is the Map A new contributor opens the `routes` folder and reads the endpoints straight from the paths: ``` routes/ ├── index.ts # GET / ├── health.ts # GET /health ├── users/ │ ├── index.ts # GET /users │ ├── [id].ts # GET /users/:id │ └── [id]/ │ └── posts.ts # GET /users/:id/posts └── orders/ └── index.ts # POST /orders ``` No registry to cross-check, no decorators to trace. The path on disk is the path on the wire, covered in [File-based Routing](/core-concepts/file-based-routing). ![The folder is the map: createPattern turns each file path directly into a URL pattern, so routes/index.ts becomes GET /, routes/users/index.ts becomes GET /users, routes/users/\[id\].ts becomes GET /users/:id, and files prefixed with an underscore are skipped as private](/diagrams/team-folder-map.png) ## A Junior Ships on Day One Adding an endpoint means adding a file. A junior developer who needs a `GET /products` route creates `routes/products/index.ts` and exports a handler: ```typescript twoslash // routes/products/index.ts import type { Context } from '@neabyte/deserve' // New endpoint, no registration needed export function GET(ctx: Context): Response { return ctx.send.json({ products: [] }) } ``` The route is live on the next save through [Hot Reload](/core-concepts/hot-reload), with no restart and no edit to a shared config file that might cause a merge conflict. ![A junior ships on day one: creating routes/products/index.ts triggers the watcher's file-created event, the module is imported and its GET handler registered, and the route answers GET /products on the next request, with no restart and no shared config edit so there is no merge conflict](/diagrams/team-junior-ships.png) ## Predictable Handlers Every route file follows the same shape, so reviewing a teammate's code needs no guesswork. The exported function name is the HTTP method, and the `Context` gives the request and the response helpers: ```typescript twoslash // routes/orders/index.ts import type { Context } from '@neabyte/deserve' // Method name is the HTTP verb export async function POST(ctx: Context): Promise { const order = await ctx.body() return ctx.send.json( { created: true, order }, { status: 201 } ) } ``` A reviewer reads `POST` and knows the verb, reads `ctx.body()` and knows the input, reads `ctx.send.json()` and knows the output. The same pattern holds across every file, which is the [developer experience](/core-concepts/philosophy#core-beliefs) the framework aims for. Details live in [Request Handling](/core-concepts/request-handling) and the [Context Object](/core-concepts/context-object). ## Shared Rules in One Place Cross-cutting concerns stay in one spot rather than scattered through handlers. One developer can own auth, another can own logging, and neither has to touch the other's route files: ```typescript twoslash // main.ts import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Security headers for every route router.use(Mware.securityHeaders()) // Auth only for the admin area router.use( '/admin', Mware.basicAuth({ users: [ { username: 'admin', password: Deno.env.get('ADMIN_PASSWORD') ?? 'change-me' } ] }) ) await router.serve(8000) ``` Handlers stay focused on their own job, while shared behavior is applied once. The full set of building blocks is in [Global Middleware](/middleware/global), and errors flow to one place through [error handling](/error-handling/object-details). ![Shared rules in one place: securityHeaders() registered with router.use(fn) reaches every route, while basicAuth() registered with router.use('/admin', fn) reaches only /admin/\*, so one developer can own auth and another own logging without touching each other's route files](/diagrams/team-shared-rules.png) ## Many Hands, One Process Larger teams often split an app into services. Deserve runs several routers in a single process, so one person can work on the API while another works on auth without separate deployments or network glue between them: ```typescript twoslash // main.ts import { Router } from '@neabyte/deserve' const api = new Router({ routesDir: './services/api/routes' }) const auth = new Router({ routesDir: './services/auth/routes' }) // Each service owns its folder and port await Promise.all([ api.serve(3001), auth.serve(3002) ]) ``` Each service has its own folder, port, and file watcher, so teams move in parallel without stepping on each other. The full pattern, including shared code and a shared error handler, is in [Multi-Service](/core-concepts/multi-service). ![Many hands, one process: a single Deno process runs an API router owned by dev A on port 3001 and an Auth router owned by dev B on port 3002, each with its own routesDir and file watcher, so the two developers work in parallel without separate deployments or network glue](/diagrams/team-many-hands.png) ## Where to Go Next * [File-based Routing](/core-concepts/file-based-routing) - how folders map to URLs * [Hot Reload](/core-concepts/hot-reload) - edits go live without a restart * [Multi-Service](/core-concepts/multi-service) - many services in one process * [Philosophy](/core-concepts/philosophy) - the thinking behind the design --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/context-object.md' description: >- The Context object passed to every handler: request access, response helpers, params, state, and cookies. --- # Context Object The `Context` object wraps the native `Request` and provides convenient methods for accessing request data, setting response headers, and sending responses. ## What is Context? Context is a wrapper around Deno's native `Request` object, and every incoming request is wrapped in one Context that flows from middleware to route handler. Working through Context instead of the raw `Request` brings: * **Lazy parsing** - data is parsed only when a method reads it * **Convenient methods** - simple APIs for common operations * **Response utilities** - built-in methods for sending responses * **Header management** - easy response header changes ## Why Context? Context avoids repeated parsing and reprocessing during the request lifecycle, since the handler receives one Context object that persists the whole way from middleware to route handler. ## Creating Context Deserve creates Context automatically when a request arrives: ```typescript twoslash import type { Context } from '@neabyte/deserve' // Deserve builds ctx for each request export function GET(ctx: Context): Response { return ctx.send.json({ message: 'Hello' }) } ``` ## Context Structure Context wraps several key pieces: 1. **Original Request** - access via `ctx.request` 2. **Parsed URL** - used internally for query params 3. **Route Parameters** - extracted from dynamic routes 4. **Response Headers** - set before sending the response ## Lazy Parsing Context parses lazily for performance, so query, body, cookie, and header data is read only when the matching method runs, and the result is cached for later calls. Reading the body is async, so a handler that awaits it becomes `async` and returns `Promise`: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function GET(ctx: Context): Promise { // Query parses on first read const query = ctx.query() // Repeat calls reuse the cache // Body parses based on Content-Type const body = await ctx.body() // Return query and body together return ctx.send.json({ query, body }) } ``` ## Request Data Access Request data is reached through Context methods, where query, params, headers, and cookies are synchronous while body readers are async: * **Query Parameters** - `ctx.query()`, `ctx.queries()` * **Route Parameters** - `ctx.param()`, `ctx.params()` * **Headers** - `ctx.header()`, `ctx.headers` * **Cookies** - `ctx.cookie()` * **Body (async)** - `await ctx.body()`, `await ctx.json()`, `await ctx.formData()`, `await ctx.text()`, `await ctx.arrayBuffer()`, `await ctx.blob()` * **URL Information** - `ctx.url`, `ctx.pathname` * **Client IP** - `ctx.ip`, `ctx.directIp` ## Response Utilities Send responses using `ctx.send`, with one method per response type: * [`ctx.send.json()`](/response/json) - JSON response * [`ctx.send.text()`](/response/text) - plain text * [`ctx.send.html()`](/response/html) - HTML content * [`ctx.send.file()`](/response/file) - file download * [`ctx.send.data()`](/response/data) - in-memory data download * [`ctx.send.stream()`](/response/stream) - stream response (ReadableStream) * [`ctx.send.redirect()`](/response/redirect) - redirect * [`ctx.send.custom()`](/response/custom) - custom response * `ctx.handleError()` - route a failure through [error handling](/error-handling/object-details) The `ctx.redirect()` shorthand maps straight to `ctx.send.redirect()`: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Shorthand for ctx.send.redirect return ctx.redirect('/new-location', 301) } ``` ## Response Headers Set response headers before sending: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { ctx.setHeader('X-Custom', 'value') ctx.setHeader('Cache-Control', 'no-cache') return ctx.send.json({ data: 'test' }) } ``` ### Setting Multiple Headers `setHeaders()` applies several headers at once: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { ctx.setHeaders({ 'X-Custom': 'value', 'Cache-Control': 'no-cache', 'X-Request-ID': 'abc123' }) return ctx.send.json({ data: 'test' }) } ``` ### URL and Pathname URL details are read directly from Context: * `ctx.url` - full URL string * `ctx.pathname` - pathname portion of the URL, such as `/api/users/123` ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { const fullUrl = ctx.url // 'http://localhost:8000/api/users/123?sort=name' const path = ctx.pathname // '/api/users/123' return ctx.send.json({ path, fullUrl }) } ``` ### Client IP The client IP is read from Context, and both values are `undefined` when the peer is unknown: * `ctx.ip` - resolved client IP, honors [`trustProxy`](/getting-started/server-configuration#client-ip-resolution) * `ctx.directIp` - direct TCP peer, ignores forwarded headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { const client = ctx.ip // real visitor IP const peer = ctx.directIp // direct connection IP return ctx.send.json({ client, peer }) } ``` ## Sharing State Context carries request-scoped state so middleware and handlers can pass values along the chain. `ctx.state` is a plain object shared for the whole request: ```typescript twoslash import type { Context } from '@neabyte/deserve' import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.use(async (ctx, next) => { // Attach a value for later handlers ctx.state.requestId = crypto.randomUUID() return await next() }) export function GET(ctx: Context): Response { // Read what middleware stored return ctx.send.json({ id: ctx.state.requestId }) } ``` For typed access, `setState` and `getState` use a key and a value type: ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Store a typed value ctx.setState('userId' as never, '123') // Read it back with the same type const userId = ctx.getState('userId' as never) ``` The `as never` on the key is deliberate, not a workaround to copy blindly. State keys are a branded type, so the framework can reserve a few names for its own wiring and reject them at compile time. A plain string does not carry that brand, and `as never` is what tells the type system this string is a valid key. The value type stays real and checked, so `getState(...)` still returns `string | undefined`. Some keys are reserved for framework wiring and are read-only through `getState`. Calling `setState` on one throws a 500 error. The reserved keys are `view`, `worker`, `session`, `setSession`, and `clearSession`. The [worker pool](/core-concepts/worker-pool) and [session middleware](/middleware/session) read their handles this way. ## Rendering Templates When the router has a `viewsDir`, Context can render DVE templates directly: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function GET(ctx: Context): Promise { // Render a template to an HTML response return await ctx.render('home.dve', { title: 'Welcome' }) } ``` `ctx.streamRender()` streams the same output for large pages. Both throw when no `viewsDir` is configured. See [Template Syntax](/rendering/syntax) for the template grammar and [Streaming Rendering](/rendering/streaming) for the streaming path. ## Error Handling `ctx.handleError()` builds an error response and is async, so a handler that calls it becomes `async` and awaits the result: ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const isAuthorized: boolean // ---cut--- export async function GET(ctx: Context): Promise { try { if (!isAuthorized) { return await ctx.handleError(401, new Error('Unauthorized')) } return ctx.send.json({ data: 'success' }) } catch (error) { return await ctx.handleError(500, error as Error) } } ``` ### How It Works `ctx.handleError()` respects the global error handler set with `router.catch()`: * **When `router.catch()` is defined** - the custom error handler runs * **When no error handler exists** - a simple response carries the status code ### Use in Middleware Middleware can call `ctx.handleError()` to trigger error handling: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() declare const isValid: boolean // ---cut--- router.use(async (ctx, next) => { if (!isValid) { // This routes through router.catch() when defined return await ctx.handleError(401, new Error('Unauthorized')) } return await next() }) ``` ## Context Lifecycle 1. **Request arrives** - Deserve creates Context with Request and URL 2. **Route matching** - route parameters are extracted and added to Context 3. **Middleware execution** - Context passes through the middleware chain 4. **Route handler** - the handler receives Context 5. **Response sent** - Context methods build the Response --- --- url: 'https://docs-deserve.neabyte.com/middleware/cors.md' description: Configure Cross-Origin Resource Sharing (CORS) policy for Deserve routes. --- # CORS Middleware > **Reference**: [MDN HTTP CORS Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) CORS (Cross-Origin Resource Sharing) middleware handles cross-origin requests by adding appropriate headers and handling preflight OPTIONS requests. ## Basic Usage Apply CORS middleware using Deserve's built-in middleware: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Allow all origins, handle preflight router.use(Mware.cors()) await router.serve(8000) ``` ## Custom CORS Configuration Configure CORS with custom options: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Tune origins, methods, headers, and cache router.use( Mware.cors({ origin: [ 'http://localhost:3000', 'https://example.com' ], methods: [ 'GET', 'POST', 'PUT', 'DELETE' ], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Custom-Header' ], credentials: true, maxAge: 3600 }) ) await router.serve(8000) ``` ## CORS Options ### `origin` Specify allowed origins: ```typescript // Single origin origin: 'https://example.com' // Multiple origins origin: [ 'https://example.com', 'https://app.example.com' ] // Allow all origins (default) origin: '*' ``` ### `methods` Specify allowed HTTP methods: ```typescript methods: [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS' ] ``` ### `allowedHeaders` Specify allowed headers: ```typescript allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Custom-Header' ] ``` ### `exposedHeaders` Specify headers exposed to the client: ```typescript exposedHeaders: [ 'X-Total-Count', 'X-Page-Count' ] ``` ### `credentials` Allow credentials in requests: ```typescript credentials: true // Allow cookies and authorization headers ``` ### `maxAge` Set preflight cache duration in seconds: ```typescript maxAge: 3600 // Cache preflight requests for 1 hour ``` ### Defaults Every option has a default, so `Mware.cors()` with no arguments allows any origin: | Option | Default | | ---------------- | -------------------------------------------------- | | `origin` | `'*'` | | `methods` | all HTTP methods | | `allowedHeaders` | `['Content-Type', 'Authorization', 'X-Requested-With']` | | `exposedHeaders` | `[]` | | `credentials` | `false` | | `maxAge` | `86400` | ## How It Works * **No Origin header** - the request passes through untouched, since it is not cross-origin. * **Preflight OPTIONS** - a matching origin gets a **204 No Content** with the CORS headers, and a non-matching origin gets a **403 Forbidden**. * **Actual request** - a matching origin receives `Access-Control-Allow-Origin` plus credentials and exposed headers when configured. * **Vary header** - `Vary: Origin` is added whenever `origin` is not the `'*'` wildcard, so caches stay correct. ## Credentials and Wildcard Setting `credentials: true` together with `origin: '*'` throws `Deno.errors.InvalidData` when the middleware is created, because browsers reject credentialed requests against a wildcard origin. Name explicit origins instead: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Credentials need explicit origins router.use( Mware.cors({ origin: ['https://app.example.com'], credentials: true }) ) ``` ## Complete Example ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Production CORS with full options router.use( Mware.cors({ origin: [ 'http://localhost:3000', 'http://localhost:5173', 'https://yourdomain.com' ], methods: [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS' ], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Requested-With', 'X-Custom-Header' ], exposedHeaders: [ 'X-Total-Count', 'X-Page-Count' ], credentials: true, maxAge: 3600 }) ) await router.serve(8000) ``` ## Common CORS Headers ### Request Headers * `Origin` - The origin making the request * `Access-Control-Request-Method` - Method for preflight requests * `Access-Control-Request-Headers` - Headers for preflight requests ### Response Headers * `Access-Control-Allow-Origin` - Allowed origins * `Access-Control-Allow-Methods` - Allowed HTTP methods * `Access-Control-Allow-Headers` - Allowed request headers * `Access-Control-Allow-Credentials` - Allow credentials * `Access-Control-Max-Age` - Preflight cache duration * `Access-Control-Expose-Headers` - Headers exposed to client --- --- url: 'https://docs-deserve.neabyte.com/middleware/csrf.md' description: >- Protect against Cross-Site Request Forgery with origin and sec-fetch-site checks. --- # CSRF Middleware > **Reference**: [MDN Cross-Site Request Forgery (CSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF) CSRF middleware blocks forged cross-site requests on state-changing methods. Safe methods (`GET`, `HEAD`, `OPTIONS`) always pass through, and every other method must match the `Origin` header or the `Sec-Fetch-Site` header. A request that matches neither rule is denied with **403 Forbidden**. ## Basic Usage Add CSRF protection with `Mware.csrf()`: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Same-origin requests pass, others denied router.use(Mware.csrf()) await router.serve(8000) ``` With no options, the allowed origin defaults to the request origin and `secFetchSite` defaults to `['same-origin']`. ## Allowing Specific Origins The `origin` option accepts a single value, a list, or a predicate: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Single trusted origin router.use(Mware.csrf({ origin: 'https://app.example.com' })) // List of trusted origins router.use( Mware.csrf({ origin: [ 'https://app.example.com', 'https://admin.example.com' ] }) ) // Predicate for custom logic router.use( Mware.csrf({ origin: (value, ctx) => value.endsWith('.example.com') }) ) ``` ## Customizing Sec-Fetch-Site The `secFetchSite` option follows the same shape and defaults to `['same-origin']`: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Accept same-origin and same-site requests router.use( Mware.csrf({ secFetchSite: [ 'same-origin', 'same-site' ] }) ) ``` ## CSRF Options | Option | Default | Description | | -------------- | ------------------ | ---------------------------------------------------- | | `origin` | request origin | Allowed `Origin` value, list, or predicate | | `secFetchSite` | `['same-origin']` | Allowed `Sec-Fetch-Site` value, list, or predicate | A predicate receives the header value and the request context, and returns `true` to allow the request: ```typescript type CsrfRulePredicate = (value: string, ctx: Context) => boolean ``` ## How It Works * **Safe methods** - `GET`, `HEAD`, and `OPTIONS` skip the check and continue. * **Origin check** - the `Origin` header is compared against the `origin` rule. * **Sec-Fetch-Site check** - the `Sec-Fetch-Site` header is compared against the `secFetchSite` rule. * **Allow** - the request passes when either check matches. * **Deny** - the request is rejected with **403 Forbidden** when neither matches. ## Error Handling When a request is blocked, the middleware returns message `Request blocked by CSRF protection` with **status code 403**. To shape that response, register a single handler with [`router.catch()`](/error-handling/object-details), or rely on the [default behavior](/error-handling/default-behavior). --- --- url: 'https://docs-deserve.neabyte.com/response/custom.md' description: >- Build fully custom responses with ctx.send.custom() when the helpers are not enough. --- # Custom Responses The `ctx.send.custom()` method creates custom responses with full control over body, status code, headers, and all response configuration options. Unlike the typed helpers, it sets no `Content-Type` on its own, so add one through the headers when the body needs it. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Status and headers stay optional return ctx.send.custom('Custom response body') } ``` ## With Status Code ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Set the response status to 404 return ctx.send.custom('Not Found', { status: 404 }) } ``` ## With Custom Headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Header set on the context ctx.setHeader('X-Custom', 'value') // Options can add more headers return ctx.send.custom('Response body', { headers: { 'Content-Type': 'application/xml', 'X-Additional': 'header' } }) } ``` ## Binary Responses ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Send raw bytes with a type const binaryData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]) return ctx.send.custom(binaryData, { headers: { 'Content-Type': 'application/octet-stream' } }) } ``` ## Empty Response (No Content) ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // 204 sends a null body return ctx.send.custom(null, { status: 204 }) } ``` ## XML Response ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // XML string with an XML type const xml = 'Hello' return ctx.send.custom(xml, { headers: { 'Content-Type': 'application/xml' } }) } ``` ## Combining Context Headers and Custom Options Headers set via `ctx.setHeader()` are merged with headers from the options parameter: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { ctx.setHeader('X-Context-Header', 'from-context') return ctx.send.custom('Body', { headers: { 'X-Options-Header': 'from-options' } }) // Response carries both headers } ``` Options headers take precedence over context headers when they conflict. --- --- url: 'https://docs-deserve.neabyte.com/response/data.md' description: Send binary data downloads with ctx.send.data(). --- # Data Download Responses The `ctx.send.data()` method sends in-memory data (string or `Uint8Array`) as a file download. Useful when content is created at runtime (generate CSV, JSON export, etc.) without writing to disk first. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // String body with a download name const csvData = 'name,age\nAlice,30\nBob,25' return ctx.send.data(csvData, 'users.csv') } ``` ## Binary Data ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Uint8Array body with a download name const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47]) return ctx.send.data(binaryData, 'image.png') } ``` ## With Custom Content Type ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Fourth arg sets the content type const jsonData = JSON.stringify({ data: 'value' }) return ctx.send.data( jsonData, 'data.json', { status: 200 }, 'application/json' ) } ``` ## Dynamic File Generation ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Build the payload at runtime const data = { timestamp: new Date().toISOString(), version: '1.0.0' } const content = JSON.stringify(data, null, 2) // Download without touching disk return ctx.send.data(content, 'metadata.json') } ``` --- --- url: 'https://docs-deserve.neabyte.com/error-handling/default-behavior.md' description: How Deserve handles uncaught errors by default and the responses it produces. --- # Default Error Behavior This error handling mechanism catches every error that occurs during server runtime, which covers route handler errors, middleware failures, route not found scenarios, static file errors, and any other uncaught exception during request processing. Without a custom error handler set through `router.catch()`, Deserve falls back to this default behavior so the server never crashes from unhandled errors. ![When an error occurs, the request routes to a custom handler if router.catch is defined, otherwise to the default handler that returns JSON or HTML by Accept, then a single response](/diagrams/default-error-behavior.png) ## Basic Default Behavior Without a call to `router.catch()`, Deserve handles every error with a default response, JSON or HTML, and the matching status code: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // No router.catch, defaults take over await router.serve(8000) ``` ## Default Error Response The default error response (without custom `router.catch()`) follows the client's `Accept` header: * **Accept includes `application/json`** → JSON body: `{ error, path, statusCode }` * **Otherwise** → HTML body: simple error page with status and message (escaped) Also: * **Status Code**: Preserves the original error status code (404, 500, etc.) * **Headers**: Includes headers set via `ctx.setHeader()` before the error ```typescript // Example default response (client requests JSON) // Status: 404 // Body: { "error": "...", "path": "/api/foo", "statusCode": 404 } // Example default response (client does not request JSON) // Status: 404 // Body: HTML with 404 and error message ``` ## Error Scenarios Default error handling covers all error types that can occur during request processing: ### 404 - Route Not Found When a route doesn't exist or no matching route handler is found: ```typescript // GET /nonexistent // Status: 404 // Body: JSON or HTML (by Accept header) // Headers: {} ``` This includes: * Non-existent routes * Routes with incorrect HTTP methods * Routes that fail to match during routing resolution ### 500 - Server Errors When a route handler throws any error or exception: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Throwing is caught by Deserve throw new Error('Something went wrong') // Default reply is 500 JSON or HTML } ``` This covers: * Uncaught exceptions in route handlers * Runtime errors (TypeError, ReferenceError, etc.) * Async operation failures * Any error thrown during handler execution ### Middleware Errors When middleware functions throw errors or fail: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // A throwing middleware is caught too router.use(async (ctx, next) => { throw new Error('Middleware failed') // Default reply is 500 JSON or HTML }) ``` All middleware errors are caught and handled by the default error handler. ### Static File Errors When serving static files encounters issues: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Serve static files at /static router.static('/static', { path: './public' }) // Missing file (GET /static/missing.jpg): // Status 404, JSON or HTML per Accept ``` This includes: * File not found errors (404) * File read permission errors (500) * Filesystem operation failures (500) * Invalid path resolution errors (500) ### Request Processing Errors Any unexpected errors during request handling: ```typescript // Errors in: // - URL parsing // - Context creation // - Route matching // - Response generation // All default to: Status 500, JSON or HTML body (by Accept) ``` ### Error Handling Guarantees The default error handler ensures: * **No server crashes**: All errors are caught and converted to HTTP responses * **Consistent behavior**: Same error response format across all error types * **Header preservation**: Headers set before the error are retained in the response * **Status code accuracy**: Original error status codes (404, 500, etc.) are preserved --- --- url: 'https://docs-deserve.neabyte.com/error-handling/defense-in-depth.md' description: Layered error handling in Deserve to keep services available under faults. --- # Defense in Depth Errors in Deserve pass through several layers, and each layer is a chance to catch, shape, or record a failure. When one layer lets an error through, the next one still holds, so the server keeps responding and never crashes. ![Five layered error defenses: route handler try/catch, WrapMware labeled catch, router.catch custom handler, default handler with masked message, and the process guard that never crashes](/diagrams/defense-in-depth.png) ## Layer 1 - Route Handler The closest layer is the handler itself. A local `try/catch` turns an expected failure into a precise response: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function POST(ctx: Context): Promise { try { const data = await ctx.body() return ctx.send.json({ success: true }) } catch (error) { // Handle the expected failure here return ctx.send.json({ error: 'Invalid body' }, { status: 400 }) } } ``` Anything thrown past this point falls to the next layer. ## Layer 2 - Labeled Middleware `WrapMware` wraps a middleware so a throw becomes a labeled error routed to the error handler. The label points straight at the failing middleware: ```typescript twoslash import { Router, WrapMware } from '@neabyte/deserve' const router = new Router() // ---cut--- // Throws here reach router.catch with a label const auth = WrapMware('Auth', async (ctx, next) => { if (!ctx.header('authorization')) { throw new Error('Missing token') } return await next() }) router.use(auth) ``` See [Global Middleware](/middleware/global#wrapping-middleware-with-error-handling) for the full pattern. ## Layer 3 - Custom Error Handler `router.catch()` receives every uncaught error and shapes the client response. It runs for handler errors, middleware errors, not-found, and static file errors alike: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.catch((ctx, error) => { // Shape one response for all errors return ctx.send.json({ error: 'Something went wrong' }, { status: error.statusCode }) }) ``` The handler receives an error object with `statusCode`, `pathname`, `url`, `method`, and the original `error`. See [Object Details](/error-handling/object-details) for each field. ## Layer 4 - Default Handler When no `router.catch()` is set, or the custom handler returns something other than a `Response`, Deserve falls back to the default handler. It negotiates JSON or HTML by the `Accept` header and **masks the original message**, so a thrown error never leaks its text to the client: ```typescript // Client gets a safe, status-based message // 500 -> "Internal Server Error" // 404 -> "Not Found" ``` The default response also carries the built-in [security headers](/middleware/security-headers). See [Default Behavior](/error-handling/default-behavior) for the full response shape. ## Layer 5 - Process Guard The outermost layer runs process-wide. A serving router traps unhandled rejections, uncaught errors, and blocked termination attempts, then reports each as a `process:error` event instead of letting the process die: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.on((event) => { if (event.kind === 'process:error') { const { origin, error } = event.metadata as { origin: string; error: Error } console.error(`process fault [${origin}]`, error.message) } }) ``` This is the safety net behind everything else. See [Process Protection](/getting-started/server-configuration#process-protection) for what it blocks and why, and [Error Reporting](/middleware/observability/errors) for how to capture these. ## Recording Across Layers Shaping a response and recording a failure are separate jobs. `router.catch()` controls what the client sees, while [`router.on()`](/middleware/observability/overview) records what happened for logs and metrics. Wire both for full coverage: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Shape the client response router.catch((ctx, info) => { return ctx.send.json({ error: 'Something went wrong' }, { status: info.statusCode }) }) // Record the failure for later router.on((event) => { if (event.kind === 'request:error') { const { url, error } = event.metadata as { url: string; error?: Error } console.error(url, error?.message) } }) ``` --- --- url: 'https://docs-deserve.neabyte.com/error-handling/object-details.md' description: Customize error responses with router.catch() and the ErrorInfo object. --- # Error Object Details Deserve provides error handling for route execution errors, validation errors, not found errors, static file errors, and custom error responses. ## Basic Error Handling Handle errors with the `router.catch()` method: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Catch errors from any route router.catch((ctx, error) => { // Reply with the error status return ctx.send.json( { error: 'Something went wrong', statusCode: error.statusCode, pathname: error.pathname, method: error.method, url: error.url }, { status: error.statusCode } ) }) await router.serve(8000) ``` ## Error Object Structure The error handler receives the context object and an error object with these properties: * **`error.statusCode`** - HTTP status code (404, 500, etc.) * **`error.pathname`** - request path, for example `/api/users` * **`error.url`** - full request URL * **`error.method`** - HTTP method * **`error.error`** - the original Error instance ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Handler reads the error object router.catch((ctx, error) => { // Fall back when no original message return ctx.send.json( { error: error.error?.message || 'An error occurred', status: error.statusCode, pathname: error.pathname, method: error.method, url: error.url }, { status: error.statusCode } ) }) ``` ## Common Error Scenarios ### 404 - Route Not Found ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.catch((ctx, error) => { if (error.statusCode === 404) { return ctx.send.json( { error: 'Route not found', pathname: error.pathname }, { status: 404 } ) } return null }) ``` ### 500 - Server Errors ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.catch((ctx, error) => { if (error.statusCode === 500) { console.error('Server error:', error.error) return ctx.send.json({ error: 'Internal server error' }, { status: 500 }) } return null }) ``` ## Route Handler Error Handling Catch errors in individual route handlers: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function POST(ctx: Context): Promise { try { const data = await ctx.body() // Process data... return ctx.send.json({ success: true }) } catch (error) { return ctx.send.json({ error: 'Failed to process request' }, { status: 500 }) } } ``` ## Validation Errors Return appropriate status codes for validation errors: ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' // ---cut--- export async function POST(ctx: Context): Promise { const data = await ctx.body() as DataRecord if (!data.email) { return ctx.send.json({ error: 'Email is required' }, { status: 400 }) } // Process valid data... return ctx.send.json({ success: true }) } ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/observability/errors.md' description: Capture and report errors from Deserve using the observability event stream. --- # Error Reporting Errors surface on the same [`router.on()`](/middleware/observability/overview) bus, so reporting lives in one listener rather than spread across handlers. ## Reporting Failed Requests `request:error` fires whenever a response status is `400` or higher, and carries the original error when one exists: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Record every failed request router.on((event) => { if (event.kind === 'request:error') { const { method, url, statusCode, error } = event.metadata as { method: string url: string statusCode: number error?: Error } console.error(`${method} ${url} ${statusCode}`, error?.message) } }) await router.serve(8000) ``` ## Capturing Process Faults `process:error` fires for unhandled rejections, uncaught errors, and blocked termination attempts. A serving router keeps running and reports the fault instead of crashing: ![Unhandled rejections, uncaught errors, and blocked self-termination each become a process:error event carrying its origin and error, so the process keeps running with no downtime and the fault is captured in the same router.on listener instead of being lost to a crash](/diagrams/obs-process-fault.png) ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // ---cut--- router.on((event) => { if (event.kind === 'process:error') { const { origin, error } = event.metadata as { origin: string; error: Error } // origin tells the fault source console.error(`process fault [${origin}]`, error.message) } }) ``` ## Pairing With Error Handling Two hooks cover different jobs: * [`router.catch()`](/error-handling/object-details) shapes the response a client receives. * `router.on()` records what happened for logs and metrics. Use `catch` to control the reply, and `on` to observe it. A typical setup wires both: ![One failed request fans out to two independent hooks, where router.catch shapes the Response the client receives with a controlled status and body, and router.on records the same failure into logs and metrics without affecting the reply](/diagrams/obs-catch-vs-on.png) ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // ---cut--- // Shape the client response router.catch((ctx, info) => { return ctx.send.json({ error: 'Something went wrong' }, { status: info.statusCode }) }) // Record the failure for later router.on((event) => { if (event.kind === 'request:error') { const { url, error } = event.metadata as { url: string; error?: Error } console.error(url, error?.message) } }) ``` For the default response when no handler is set, see [Default Behavior](/error-handling/default-behavior). --- --- url: 'https://docs-deserve.neabyte.com/middleware/observability/events.md' description: >- Reference of all lifecycle and error events emitted by a serving Deserve router. --- # Event Reference Every event from [`router.on()`](/middleware/observability/overview) carries a `kind` discriminant and a `metadata` object. This page lists each kind and the fields it provides. ![A request event is external by default but becomes internal when a timeout, a framework error, or a missing context produced it, while every non-request kind is always internal, so routing on the type field keeps normal client traffic out of the fault alert channel](/diagrams/obs-event-channel.png) ## Server | Kind | Metadata | | ------------------- | ------------------------- | | `server:listening` | `port`, `hostname` | | `server:shutdown` | none | `server:listening` fires once the server binds. `server:shutdown` fires after the server stops draining. ## Routes | Kind | Metadata | | ---------------- | --------------------------------- | | `route:loaded` | `routePath`, `pattern` | | `route:reloaded` | `routePath`, `pattern` | | `route:removed` | `routePath`, `pattern` | | `route:skipped` | `routePath`, `reason` | | `route:error` | `routePath`, `error` | | `reload:error` | `routePath`, `error` | Reload events come from hot reload as files change on disk. ## Views | Kind | Metadata | | ---------------- | ------------------------- | | `view:compiled` | `path`, `durationMs` | | `view:rendered` | `path`, `durationMs` | | `view:refreshed` | `paths` | | `view:error` | `path`, `error` | View events come from the [DVE rendering engine](/rendering/). ## Requests | Kind | Metadata | | ------------------- | --------------------------------------------------- | | `request:complete` | `method`, `statusCode`, `url`, `durationMs`, metrics | | `request:error` | same as `request:complete`, plus `error` | `request:complete` fires for every finished request. `request:error` fires in addition whenever the status is `400` or higher. Both carry optional OpenTelemetry-aligned metrics when known: `route`, `serverAddress`, `serverPort`, `userAgent`, `requestSize`, `responseSize`, and `ip`. Turn these into a log in [Request Logging](/middleware/observability/logging). ## Process | Kind | Metadata | | --------------- | -------------------------------------------------------------------- | | `process:error` | `error`, `origin` (`unhandledrejection`, `uncaughterror`, `process:exit`) | A serving router traps unhandled rejections, uncaught errors, and attempts to terminate the process. Each fault becomes a `process:error` event rather than crashing the server, so a single failure never takes the process down. A blocked termination call carries `origin: 'process:exit'` and names the call, for example `Blocked Deno.exit(0) - process termination is not permitted from application code`. See [Process Protection](/getting-started/server-configuration#process-protection) for the reasoning, and capture these in [Error Reporting](/middleware/observability/errors). --- --- url: 'https://docs-deserve.neabyte.com/examples.md' --- # Examples ## Showcase * **[Deserve-React](https://github.com/NeaByteLab/Deserve-React)** - Deserve + React with Vite SSR. File-based routes, cookie session, in-memory CRUD demo. * **[Deserve-VE](https://github.com/NeaByteLab/Deserve-VE)** - Deserve + DVE (Deno View Engine). File-based routes, `.dve` templates, partials, static assets. * **[Restful-API](https://github.com/NeaByteLab/Restful-API)** - RESTful API with Deserve and [Jsonary](https://jsr.io/@neabyte/jsonary) (file-based JSON DB). Full CRUD over a users resource. ## Where to Start * **[Quick Start](/getting-started/quick-start)** - Run the first server and route. * **[Server Configuration](/getting-started/server-configuration)** - Configure options, middleware, and static files. --- --- url: 'https://docs-deserve.neabyte.com/response/file.md' description: Serve file downloads from the filesystem with ctx.send.file(). --- # File Download Responses The `ctx.send.file()` method sends file contents from the filesystem as the response. Suitable for downloads or serving files already on disk (relative or absolute path). ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export async function GET(ctx: Context): Promise { // Stream the file as a download return await ctx.send.file('./uploads/document.pdf') } ``` ## With Custom Filename ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function GET(ctx: Context): Promise { // Second arg renames the download return await ctx.send.file('./files/data.csv', 'report.csv') } ``` ## Error Handling A missing or unreadable file throws `Deno.errors.NotFound`. Catch it in the handler for a precise reply, or let it bubble to the [centralized error handler](/error-handling/object-details): ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function GET(ctx: Context): Promise { try { return await ctx.send.file('./uploads/document.pdf') } catch (error) { // Missing file throws, reply 404 return ctx.send.json({ error: 'File not found' }, { status: 404 }) } } ``` --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/file-based-routing.md' description: >- How Deserve maps the routes directory structure to HTTP endpoints using file-based routing. --- # File-based Routing > **Reference**: [Deno File-based Routing Tutorial](https://docs.deno.com/examples/file_based_routing_tutorial/) File-based routing is Deserve's core concept, where the file system structure becomes the API structure automatically, following the same pattern as [Next.js](https://nextjs.org/) but for Deno APIs. ## How It Works Deserve scans the routes directory and creates endpoints from the file structure, and every supported extension (`.ts`, `.js`, `.tsx`, `.jsx`, `.mjs`, `.cjs`) works the same way: ``` routes/ ├── index.ts → GET / ├── about.mjs → GET /about ├── users.js → GET /users ├── users/[id].ts → GET /users/:id └── users/[id]/ └── posts/ └── [postId].jsx → GET /users/:id/posts/:postId ``` ## Core Rules ### 1. File Names Become Routes * `index.ts`, `index.js`, `index.mjs` → `/` (root) * `about.ts`, `about.js`, `about.mjs` → `/about` * `users.ts`, `users.js`, `users.cjs` → `/users` All supported extensions (`.ts`, `.js`, `.tsx`, `.jsx`, `.mjs`, `.cjs`) work identically. ### 2. Folders Create Nested Routes * `users/[id].ts` → `/users/:id` * `users/[id]/posts.ts` → `/users/:id/posts` ### 3. Dynamic Parameters Use `[param]` Syntax * `[id].ts` → `:id` parameter * `[userId].ts` → `:userId` parameter * `[postId].ts` → `:postId` parameter Dynamic segments are matched by [Route Patterns](/core-concepts/route-patterns) and read with `ctx.param()` from [Request Handling](/core-concepts/request-handling#route-parameters). ### 4. HTTP Methods Are Exported Functions ```typescript twoslash import type { Context } from '@neabyte/deserve' // Each export maps to its method export function GET(ctx: Context): Response { return ctx.send.json({ users: [] }) } export async function POST(ctx: Context): Promise { const data = await ctx.body() return ctx.send.json({ message: 'User created', data }) } // PUT, PATCH, DELETE follow the same shape // export function [method](ctx: Context): Response { ... } ``` ### 5. Case-Sensitive URLs URLs are case-sensitive following HTTP standards: * `/Users/John` ≠ `/users/john` * `/API/v1` ≠ `/api/v1` ### 6. Valid Filename Characters Files can contain specific rules: * `a-z`, `A-Z`, `0-9` - Alphanumeric characters * `_` - Underscore (do not prefix path segment - see below) * `-` - Dash * `.` - Dot * `~` - Tilde * `+` - Plus sign * `[` `]` - Brackets for dynamic parameters **Skipped segments:** Folders or file names that **start with** `_` or `@` are not registered as routes (e.g. `_layout.ts`, `@middleware.ts`, folder `_components/`). Useful for support files that are not endpoints. Edited route files reload on the fly without a restart, covered in [Hot Reload](/core-concepts/hot-reload). --- --- url: 'https://docs-deserve.neabyte.com/middleware/global.md' description: Register global middleware that runs for every request with router.use(). --- # Global Middleware Global middleware executes for every request before route handlers, providing cross-cutting functionality like authentication, logging, and CORS. Each `router.use(fn)` call appends an entry with an empty path, so it matches every request and runs in the exact order you registered it, before any route matching happens. ![Global Middleware registration and position: each router.use(fn) appends a path-empty entry that matches every request and runs before route matching, in registration order](/diagrams/middleware-global-registration.png) ## Basic Usage Add global middleware using the `use()` method: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // Log every request, then continue router.use(async (ctx, next) => { console.log(`${ctx.request.method} ${ctx.url}`) return await next() }) await router.serve(8000) ``` ## Middleware Function Signature ```typescript type MiddlewareFn = ( ctx: Context, next: () => Promise ) => Response | undefined | Promise ``` * **Return `await next()`** - continue to the next middleware or route handler, which allows response modification and inspection. * **Return `Response`** - stop processing and return that response immediately. * **Return `undefined`** - treated as pass-through so the chain continues as if `next()` were called. Middleware must either call `next()` and use its result or return a `Response`. When it does neither, for example never calling `next()` and returning nothing, the request can hang, so `requestTimeoutMs` in `Router` caps the request duration and returns a 503 instead. ![Global Middleware per-request control flow: return await next() continues the chain, returning a Response stops and skips the handler, returning undefined is pass-through; throwing routes to router.catch or 500, and stalling triggers the requestTimeoutMs 503 guard](/diagrams/middleware-global-flow.png) ## Common Global Middleware Patterns ### Request Logging ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.use(async (ctx, next) => { const start = Date.now() console.log(`${ctx.request.method} ${ctx.url} - ${new Date().toISOString()}`) const response = await next() const duration = Date.now() - start console.log(`Completed in ${duration}ms`) return response }) ``` ### Authentication ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() declare function isValidToken(token: string): boolean // ---cut--- router.use(async (ctx, next) => { const authHeader = ctx.header('authorization') if (!authHeader) { return ctx.send.text('Unauthorized', { status: 401 }) } // Validate token here... const token = authHeader.replace('Bearer ', '') if (!isValidToken(token)) { return ctx.send.text('Invalid token', { status: 401 }) } return await next() }) ``` ## Wrapping Middleware With Error Handling Custom middleware that throws can be wrapped with `WrapMware`, so errors are caught and passed to `router.catch()` when it is defined: ```typescript twoslash import { Router, WrapMware } from '@neabyte/deserve' const router = new Router() // Wrap so throws reach router.catch const myAuth = WrapMware('Auth', async (ctx, next) => { if (!ctx.header('x-api-key')) { throw new Error('Missing API key') } return await next() }) // Apply middleware and the error handler router.use(myAuth) router.catch((ctx, err) => ctx.send.json({ error: err.error?.message }, { status: 500 })) await router.serve(8000) ``` **Signature:** `WrapMware(label: string, middleware: MiddlewareFn): MiddlewareFn`. When the middleware throws, the error runs through `ctx.handleError()` so `router.catch()` is invoked. ## Path-Specific Middleware Middleware also applies to specific paths: ```typescript twoslash import type { Context } from '@neabyte/deserve' import { Router } from '@neabyte/deserve' const router = new Router() declare function isAuthenticated(ctx: Context): boolean // ---cut--- // Runs only for /api paths router.use('/api', async (ctx, next) => { console.log('API request:', ctx.url) return await next() }) // Guard /admin paths with an auth check router.use('/admin', async (ctx, next) => { if (!isAuthenticated(ctx)) { return ctx.send.text('Unauthorized', { status: 401 }) } return await next() }) ``` --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/hot-reload.md' description: >- Hot reload in Deserve: how route and template changes are detected and applied without restarting the server. --- # Hot Reload Deserve automatically watches the `routesDir` and `viewsDir` directories for file changes, and when a file is created, modified, or deleted the server picks up the change on the next request with no restart required. ## Zero Configuration Hot reload starts automatically when the server starts: ```typescript twoslash import { Router } from '@neabyte/deserve' const app = new Router({ routesDir: './routes', viewsDir: './views' }) // Watchers start automatically app.serve(3000) ``` ## What Gets Watched ### Route Files All files with supported extensions (`.ts`, `.js`, `.tsx`, `.jsx`, `.mjs`, `.cjs`) inside `routesDir` are watched recursively. | Event | Behavior | | ----------------- | ---------------------------------------------------------------------------- | | **File created** | Module is imported and route handlers are registered automatically | | **File modified** | Old handlers are removed, module is re-imported, new handlers are registered | | **File deleted** | Route pattern is removed from the router, requests return 404 | ### Template Files All `.dve` files inside `viewsDir` are watched recursively, so [template](/rendering/) edits show on the next render without a restart. | Event | Behavior | | ----------------- | ------------------------------------------------------------------------------ | | **File created** | Discovered paths are refreshed so the template is available for rendering | | **File modified** | File cache and compiled AST cache are cleared, next render reads fresh content | | **File deleted** | Discovered paths are refreshed, rendering the template will throw an error | ## Error Isolation Bad files are caught, logged, and never crash the server or other routes. Each failure also surfaces as a [`route:error` or `reload:error`](/middleware/observability/events#routes) observability event, so logging stays in one place. ![An abstract view of why reloading stays safe, where applying a file change live rests on three mechanisms that hold together, isolating each file with a try catch so a bad one never crashes the others, busting the module cache with a timestamp query so stale code never contaminates the new, and reloading in sequence by removing then registering after a debounce, which together deliver live edits with no downtime, no crash, and no contamination](/diagrams/hot-reload-principles.png) ### Malformed Syntax Invalid syntax fails the import and logs the error. Other routes stay unaffected: ``` [Deserve] Failed to reload route malformed.ts: The module's source code could not be parsed: Expected ';', '}' or at ... ``` ### Missing HTTP Method Exports Routes without a valid HTTP method export (`GET`, `POST`, etc.) are rejected and logged: ``` [Deserve] Failed to reload route broken.ts: Route "broken.ts" must export at least one HTTP method (DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT) ``` ### Runtime Errors in Handlers If a reloaded handler throws at request time, Deserve's [error handling](/error-handling/defense-in-depth) returns a proper 500 response. The server stays alive and other routes are unaffected. ## Debouncing File system events are debounced to prevent redundant reloads during rapid saves: * **Template watcher**: 100ms debounce, clears only the changed file's cache entries * **Route watcher**: 150ms debounce, batches multiple file changes into a single sequential reload Multiple file changes within the debounce window are batched into a single operation, avoiding redundant reloads when saving several files at once. ## How It Works ### Route Reloading ![The route reload sequence as the watcher runs it, where Deno.watchFs detects a change and debounces for 150ms, FastRouter.remove drops the old pattern, the module is re-imported with a timestamp query to bypass the cache, then it is validated for an HTTP method and its handlers register while emitting route:reloaded, and any failure in that step instead emits reload:error so the server stays alive and other routes are unaffected](/diagrams/hot-reload-route-sequence.png) 1. `Deno.watchFs` detects a change in `routesDir` 2. After the debounce window, the watcher resolves the file path to a route pattern 3. The old route pattern is removed from the router via `FastRouter.remove()` 4. The module is re-imported with a cache-busting query string (`?t=timestamp`) to bypass Deno's module cache 5. The module is validated and new HTTP method handlers are registered ### Template Reloading 1. `Deno.watchFs` detects a change in `viewsDir` 2. After the debounce window, the watcher clears the file's entry from `fileCache` (raw content) and `compileCache` (parsed AST) 3. The discovered template paths set is reset 4. On the next `render()` or `streamRender()` call, the engine re-reads the file from disk, re-parses it, and caches the result --- --- url: 'https://docs-deserve.neabyte.com/response/html.md' description: Send HTML responses with ctx.send.html(). --- # HTML Responses The `ctx.send.html()` method creates HTML responses. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Sends text/html by default const html = '

Hello World

' return ctx.send.html(html) } ``` ## Dynamic HTML ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Build markup with a template literal const html = ` Welcome

Hello from Deserve!

Server is running

` return ctx.send.html(html) } ``` ## With Status Codes ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Not Found page with status 404 const html = '

Not Found

' return ctx.send.html(html, { status: 404 }) } ``` ## Custom Headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Set a header before sending ctx.setHeader('X-Frame-Options', 'DENY') return ctx.send.html('Content') } ``` --- --- url: 'https://docs-deserve.neabyte.com/getting-started/installation.md' description: Install Deserve into a Deno project using the JSR package registry. --- # Installation Add Deserve to a Deno project in one command, then move on to the ideas behind it in [Core Concepts](/core-concepts/philosophy), starting with the [philosophy](/core-concepts/philosophy) and [zero dependency](/core-concepts/zero-dependency) approach. ## Prerequisites * [Deno](https://github.com/denoland/deno_install) 2.7.0+ installed Staying on the latest Deno release is a good idea, since Deserve runs on the runtime and every performance update to Deno carries straight through to Deserve. ## Install Deserve Deno's package manager adds Deserve to the project. This command writes the dependency into `deno.json` and generates `deno.lock`: ::: code-group ```bash [deno] deno add jsr:@neabyte/deserve ``` ::: The command does three things: * Adds Deserve to the `deno.json` imports * Creates or updates the `deno.lock` file * Makes Deserve available for import With Deserve installed, the [Quick Start](/getting-started/quick-start) builds a first server and route, and [File-based Routing](/core-concepts/file-based-routing) explains how the folder structure becomes the API. --- --- url: 'https://docs-deserve.neabyte.com/middleware/ip.md' description: >- Restrict access by IP address using whitelist and blacklist rules with CIDR support. --- # IP Restriction Middleware IP restriction middleware allows or denies requests by the resolved client IP address. The whitelist takes precedence, the blacklist runs next, and the check fails safe by denying any request with an unknown IP. Each rule accepts an exact address, a CIDR range, or the `*` wildcard, and both IPv4 and IPv6 are supported. ## Basic Usage Allow only trusted addresses with `Mware.ip()`: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Allow only listed addresses router.use( Mware.ip({ whitelist: [ '127.0.0.1', '192.168.1.0/24' ] }) ) await router.serve(8000) ``` ## Blocking Addresses Use `blacklist` to deny specific addresses while allowing the rest: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Deny listed addresses, allow others router.use( Mware.ip({ blacklist: [ '203.0.113.5', '198.51.100.0/24' ] }) ) ``` ## Rule Formats Each entry in `whitelist` or `blacklist` can be one of these: ```typescript // Exact IPv4 or IPv6 address '127.0.0.1' '::1' // CIDR range '10.0.0.0/8' 'fc00::/7' // Wildcard, matches every address '*' ``` A malformed rule throws `Deno.errors.InvalidData` when the middleware is created. ## IP Options | Option | Default | Description | | ----------- | ------- | ------------------------------------ | | `whitelist` | - | Allowed IP, CIDR, or wildcard rules | | `blacklist` | - | Denied IP, CIDR, or wildcard rules | ## How It Works * **Unknown IP** - a request with no resolved IP is denied. * **Whitelist present** - only IPs that match the whitelist pass, everything else is denied. * **Blacklist present** - IPs that match the blacklist are denied, the rest pass. * **Neither set** - every request passes. The middleware reads the resolved client IP from `ctx.ip`. Behind a proxy, configure [`trustProxy`](/getting-started/server-configuration#client-ip-resolution) so the real visitor IP is used. ## Error Handling When a request is denied, the middleware returns message `Access denied by IP restriction` with **status code 403**. To shape that response, register a single handler with [`router.catch()`](/error-handling/object-details), or rely on the [default behavior](/error-handling/default-behavior). --- --- url: 'https://docs-deserve.neabyte.com/response/json.md' description: 'Send JSON responses with ctx.send.json(), including status codes and headers.' --- # JSON Responses The `ctx.send.json()` method creates JSON responses. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Sends application/json by default return ctx.send.json({ message: 'Hello World' }) } ``` ## With Status Codes ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export async function POST(ctx: Context): Promise { const data = await ctx.body() // Reply Created with status 201 return ctx.send.json( { message: 'Created successfully', data }, { status: 201 } ) } ``` The `status` value must be an integer in the 200-599 range, or one of the body-less codes 101, 204, 205, and 304 which send an empty body. Any other value throws `Deno.errors.InvalidData`. This rule is shared by every `ctx.send` helper. ## With Custom Headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Set a header before sending ctx.setHeader('Cache-Control', 'no-cache') return ctx.send.json({ data: 'sensitive' }) } ``` ## Complex Data ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Nested objects serialize as-is const data = { users: [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ], pagination: { page: 1, total: 2, hasNext: false }, timestamp: new Date().toISOString() } return ctx.send.json(data) } ``` ## Error Responses ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Error body with status 404 return ctx.send.json( { error: 'User not found' }, { status: 404 } ) } ``` --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/multi-service.md' description: Running multiple Deserve services side by side in a single Deno process. --- # Multi-Service Deserve runs multiple servers from a single Deno process. Each `Router` is a standalone server with its own routes, middleware, file watcher, and port. They are isolated at the request level, so a fault in one never bleeds into another, yet they share the same process memory, which lets them share code, state, and infrastructure without any network overhead. Traditionally, running 5 services means 5 processes, 5 deployments, and 5 copies of the shared code. With Deserve, one `main.ts` spawns as many routers as memory can hold, and each one listens on its own port and watches its own directory while a fault in one is contained instead of taking down the rest. ![One Deno process running an API, Auth, and Web router, each on its own port with a client connecting to each](/diagrams/process-overview.png) ## Basic Setup One `Router` per service, one port per router, one `Promise.all` to start them all: ```typescript twoslash import { Router } from '@neabyte/deserve' // One Router per service const api = new Router({ routesDir: './services/api/routes' }) const auth = new Router({ routesDir: './services/auth/routes' }) const web = new Router({ routesDir: './services/web/routes', viewsDir: './services/web/views' }) // Run every service together await Promise.all([ api.serve(3001), auth.serve(3002), web.serve(3003) ]) ``` That is the entire entry point. ## Router Isolation Every `Router` runs in request-level isolation. Each one owns its own radix-tree router, middleware stack, Superwatcher instance, and optional template engine. They do not share any internal state unless it is explicitly wired up, while the process underneath is still shared, which is what makes the [shared code and state](#sharing-code-and-state) below possible. Faults are contained at two levels. A throw inside one handler becomes an error response for that one request, so the rest of that service and every other service keep serving. A deeper fault that escapes a handler, like an unhandled rejection or an attempt to exit the process, is trapped process-wide by [process protection](/getting-started/server-configuration#process-protection) and surfaced as an event rather than a shutdown, so no service goes down. ![Each router keeps its own FastRouter, middleware, and watcher in isolation, with the Web router also holding a DVE engine](/diagrams/router-isolation.png) If a route in API throws, only that request gets a 500. Auth and Web, and every other API request, keep serving normally. ## Directory Structure Every service follows the same folder convention. A new team member sees this layout and immediately knows where routes, views, and shared code live. No guessing, no project-specific conventions to learn. ``` project/ ├── main.ts ├── shared/ │ ├── utils.ts │ ├── sessions.ts │ ├── bus.ts │ ├── cache.ts │ ├── logger.ts │ └── errors.ts └── services/ ├── api/ │ └── routes/ │ ├── health.ts # GET :3001/health │ ├── me.ts # GET :3001/me │ └── users/ │ ├── index.ts # GET :3001/users │ └── [id].ts # GET :3001/users/:id ├── auth/ │ └── routes/ │ ├── login.ts # POST :3002/login │ ├── logout.ts # POST :3002/logout │ └── verify.ts # GET :3002/verify └── web/ ├── routes/ │ └── index.ts # GET :3003/ └── views/ └── home.dve ``` * Routes go in `services//routes/` * Shared code goes in `shared/` * `main.ts` wires everything together ## Sharing Code and State Sharing one process is where the multi-service model pays off. Instead of Redis, HTTP calls, or a message broker, the services share state through plain objects in memory at the speed of a function call. ![Services importing shared modules and communicating through an in-process session store, event bus, and cache](/diagrams/shared-code-state.png) ### Shared Modules Utility functions, database connections, configuration, validation schemas - write once in `shared/`, import from any service: ```typescript twoslash // shared/utils.ts // Shared helpers and constants export function formatDate(date: Date): string { return date.toISOString().split('T')[0]! } export const APP_NAME = 'MyApp' ``` ```typescript // services/api/routes/index.ts // Import shared code, no HTTP hop import type { Context } from '@neabyte/deserve' import { APP_NAME } from '../../../shared/utils.ts' // Use the shared constant here export function GET(ctx: Context): Response { return ctx.send.json({ app: APP_NAME, service: 'api' }) } ``` ### Session Store A single `Map` serves as a session store for all services. Auth writes sessions on login, API reads them to authenticate requests. No Redis, no HTTP call between services: ```typescript twoslash // shared/sessions.ts // In-memory store shared by services export const sessions = new Map>() ``` ```typescript // services/auth/routes/login.ts import type { Context } from '@neabyte/deserve' import { sessions } from '../../../shared/sessions.ts' // Auth saves the session on login export async function POST(ctx: Context): Promise { const body = (await ctx.json()) as { username?: string } const id = crypto.randomUUID() sessions.set(id, { username: body?.username, loggedInAt: Date.now() }) return ctx.send.json({ sessionId: id }) } ``` ```typescript // services/api/routes/me.ts import type { Context } from '@neabyte/deserve' import { sessions } from '../../../shared/sessions.ts' // API reads the same store directly export function GET(ctx: Context): Response { const id = ctx.header('x-session-id') const session = id ? sessions.get(id) : undefined if (!session) { return ctx.send.json({ error: 'Not authenticated' }, { status: 401 }) } return ctx.send.json({ user: session }) } ``` ### Event Bus When API creates a user, Auth and Web can know about it instantly, with no message queue and no polling, just a direct function call across services. This bus carries application facts like `user:created`. For framework activity such as requests, routes, and faults, use the built-in [observability events](/middleware/observability/overview) instead. ![The API service emits an event to the EventBus, which notifies the Auth and Web services](/diagrams/event-bus.png) ```typescript twoslash // shared/bus.ts // Minimal in-process event bus type Listener = (...args: unknown[]) => void const listeners = new Map>() export function emit(event: string, ...args: unknown[]): void { for (const fn of listeners.get(event) ?? []) { fn(...args) } } export function on(event: string, fn: Listener): void { if (!listeners.has(event)) { listeners.set(event, new Set()) } listeners.get(event)!.add(fn) } ``` ```typescript // services/api/routes/users/index.ts import type { Context } from '@neabyte/deserve' import { emit } from '../../../../shared/bus.ts' // Emit an event after creating a user export async function POST(ctx: Context): Promise { const user = await ctx.json() emit('user:created', user) return ctx.send.json({ created: true }) } ``` Any service can listen with `on('user:created', ...)` in `main.ts` or inside its own routes. ### Cache A shared `Map` with TTL eliminates duplicate work. API computes and caches, Web reads the cached result. Zero network cost: ```typescript twoslash // shared/cache.ts // Shared cache with expiry per entry const store = new Map() export function get(key: string): T | undefined { const entry = store.get(key) if (!entry || entry.expires < Date.now()) { store.delete(key) return undefined } return entry.value as T } export function set(key: string, value: unknown, ttlMs: number): void { store.set(key, { value, expires: Date.now() + ttlMs }) } ``` ### HTTP Between Services When one service needs to call another service's HTTP endpoint (not just shared code), use `fetch`. Both services are in the same process, so the call stays on localhost: ```typescript twoslash // services/web/routes/dashboard.ts import type { Context } from '@neabyte/deserve' // Call API service, then render template export async function GET(ctx: Context): Promise { const users = await fetch('http://localhost:3001/users').then((r) => r.json()) return await ctx.render('dashboard.dve', { users }) } ``` ### The Trade-off Shared state is a feature, not a free lunch. [Router isolation](#router-isolation) keeps a fault inside one service, yet a shared `Map` is the opposite of isolation by design, since every service reads and writes the same object. One service that writes bad data into the store hands that same bad data to every other reader, so coupling moves from the network layer down to the data layer. Faults stay contained, but data does not. Keep the blast radius small by letting one module own each store and validating writes at its edge, the way `shared/sessions.ts` is the single door to the session map. Reach for shared state when speed matters and the services genuinely belong together, and fall back to [HTTP between services](#http-between-services) when a cleaner boundary is worth the hop. ## Middleware Each router has its own middleware stack, so services configure independently with different middleware each, or share the same middleware across all of them. This is where the single-process model pays off, since one logger, one error handler, and one auth check apply wherever they are needed. The mechanics of registering middleware live in [Global Middleware](/middleware/global) and [Route-specific Middleware](/middleware/route-specific), and this section focuses on applying them across many services. ### Per-Service Configuration One service can have CORS and body limits, another can have security headers, and a third can run with no middleware at all: ![Each service composes its own middleware chain before its routes: API runs CORS then BodyLimit, Auth runs SecHeaders, Web runs routes with DVE](/diagrams/per-service-middleware.png) ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' // API gets CORS and a body limit const api = new Router({ routesDir: './services/api/routes' }) api.use(Mware.cors({ origin: '*' })) api.use(Mware.bodyLimit({ limit: 5 * 1024 * 1024 })) // Auth gets security headers const auth = new Router({ routesDir: './services/auth/routes' }) auth.use(Mware.securityHeaders({ xFrameOptions: 'DENY' })) // Web runs without middleware const web = new Router({ routesDir: './services/web/routes', viewsDir: './services/web/views' }) // Run every service together await Promise.all([ api.serve(3001), auth.serve(3002), web.serve(3003) ]) ``` ### Shared Logger Write one logger, apply it to every service. All requests across all ports flow through the same function, tagged by service name. One console, one format, one place to search when something goes wrong: ```typescript twoslash // shared/logger.ts // One logger reused by every service import type { MiddlewareFn } from '@neabyte/deserve' export function logger(service: string): MiddlewareFn { return async (ctx, next) => { const start = Date.now() const response = await next() const duration = Date.now() - start const status = response?.status ?? 0 console.log(`[${service}] ${ctx.request.method} ${ctx.pathname} ${status} ${duration}ms`) return response } } ``` Output from all services in one stream: ``` [API] GET /users 200 3ms [Auth] POST /login 200 12ms [Web] GET / 200 5ms [API] GET /users/99 404 1ms ``` ### Shared Error Handler One error handler applies with [`router.catch()`](/error-handling/object-details), so every thrown error, 404, or 500 across all services produces the same error shape, and the response stays predictable regardless of which service returned it: ```typescript twoslash // shared/errors.ts // One error handler shape for all import type { Context, ErrorInfo, ErrorMiddleware } from '@neabyte/deserve' export function errorHandler(service: string): ErrorMiddleware { return (ctx: Context, error: ErrorInfo): Response | null => { console.error( `[${service}] ${error.method} ${error.pathname} ${error.statusCode} - ${error.error?.message}` ) return ctx.send.json( { service, error: error.error?.message ?? 'Unknown error', statusCode: error.statusCode, path: error.pathname }, { status: error.statusCode } ) } } ``` ### Wrapping Middleware with Labels `WrapMware` tags an individual middleware with a label, so when that middleware throws, the error log includes the label and points straight to which middleware in which service failed. Its signature and base behavior are covered in [Global Middleware](/middleware/global#wrapping-middleware-with-error-handling), and it acts as one layer in [Defense in Depth](/error-handling/defense-in-depth): ```typescript // main.ts import { Router, WrapMware } from '@neabyte/deserve' import { logger } from './shared/logger.ts' import { errorHandler } from './shared/errors.ts' // Label each middleware for error logs const apiAuth = WrapMware('APIAuth', async (ctx, next) => { if (!ctx.header('authorization')) { throw new Error('Missing API key') } return await next() }) const authRateLimit = WrapMware('AuthRateLimit', async (ctx, next) => { // rate limit logic return await next() }) const webCache = WrapMware('WebCache', async (ctx, next) => { // cache logic return await next() }) // Wire logger, middleware, error handler const api = new Router({ routesDir: './services/api/routes' }) api.use(logger('API')) api.use(apiAuth) api.catch(errorHandler('API')) const auth = new Router({ routesDir: './services/auth/routes' }) auth.use(logger('Auth')) auth.use(authRateLimit) auth.catch(errorHandler('Auth')) const web = new Router({ routesDir: './services/web/routes', viewsDir: './services/web/views' }) web.use(logger('Web')) web.use(webCache) web.catch(errorHandler('Web')) // Run every service together await Promise.all([ api.serve(3001), auth.serve(3002), web.serve(3003) ]) ``` When `apiAuth` throws, the log reads `[API] GET /users 500 - APIAuth - Missing API key`. When `authRateLimit` throws, it reads `[Auth] POST /login 500 - AuthRateLimit - Too many requests`. Service name, route, and middleware label - all in one line. ### OpenTelemetry Since every request already flows through shared middleware, plugging in OpenTelemetry follows the same pattern. One OTel middleware applies to every service, so all spans from all ports go to one collector, which gives distributed tracing, latency dashboards, and error rate metrics across the entire system without instrumenting each service separately: ![A single OTel middleware collects spans from every service and exports them to an OTel Collector, then on to Jaeger, Grafana, or Datadog](/diagrams/observability.png) ```typescript twoslash // shared/otel.ts // One OTel middleware for all services import type { MiddlewareFn } from '@neabyte/deserve' export function otelMiddleware(service: string): MiddlewareFn { return async (ctx, next) => { const start = performance.now() const response = await next() const duration = performance.now() - start const status = response?.status ?? 0 // Emit a span, swap for OTel SDK console.log(JSON.stringify({ traceId: crypto.randomUUID(), service, method: ctx.request.method, path: ctx.pathname, status, durationMs: Math.round(duration * 100) / 100, timestamp: new Date().toISOString() })) return response } } ``` ## Hot Reload Each service has its own file watcher, so saving a file reloads only the service that owns that directory while the other services keep serving requests without interruption. For full details on how hot reload works, see [Hot Reload](/core-concepts/hot-reload). * **Edit** `services/api/routes/users/index.ts` (only **:3001** reloads the route) * **Add** `services/auth/routes/reset.ts` (only **:3002** picks up the new route) * **Edit** `services/web/views/home.dve` (only **:3003** clears template cache) A team can work on different services at the same time, with one person refactoring API routes, another fixing Auth logic, and a third updating Web templates, all without stepping on each other. ## Deployment ### Docker All services run in one container. One image, one process, all ports: ```dockerfile FROM denoland/deno:2.7.0 WORKDIR /app COPY . . RUN deno cache main.ts EXPOSE 3001 3002 3003 CMD ["deno", "run", "-A", "main.ts"] ``` ### Reverse Proxy Put Nginx or Caddy in front to route by domain to each service port: ![A reverse proxy such as Nginx or Caddy maps each hostname to a per-service port: api.example.com to 3001, auth.example.com to 3002, and example.com to 3003](/diagrams/reverse-proxy.png) ```nginx # API service server { server_name api.example.com; location / { proxy_pass http://127.0.0.1:3001; } } # Auth service server { server_name auth.example.com; location / { proxy_pass http://127.0.0.1:3002; } } # Web service server { server_name example.com; location / { proxy_pass http://127.0.0.1:3003; } } ``` ## Scaling Out When a service outgrows the monolith, it extracts into its own process. Copy the folder, add a `main.ts`, and deploy independently. The route files do not change, since the `Router` API is the same whether one service runs or ten: ![Extracting services from a single process into separate API, Auth, and Web processes](/diagrams/scaling-out.png) * Copy `services/api/` to a new repository * Add its own `main.ts` with a single `Router` * Deploy independently Start with everything in one process, and split when the need arises. --- --- url: 'https://docs-deserve.neabyte.com/static-file/multiple.md' description: >- Serve static assets from multiple directories under different URL prefixes in Deserve. --- # Multiple Directories Serve static files from multiple directories with different configurations per path. Each call shares the same options and resolution rules covered in [Basic Static Serving](/static-file/basic). ## Basic Usage Configure multiple static directories: ![Three static calls each bind one url prefix to its own folder with its own cache policy, where slash admin serves the admin slash dist folder with etag on and a one day cache, slash uploads serves the uploads folder with etag off and no cache, and slash docs serves the docs slash build folder with etag on and a one hour cache](/diagrams/static-multiple-dirs.png) ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // Each path gets its own folder and cache router.static('/admin', { path: './admin/dist', etag: true, cacheControl: 86400 }) router.static('/uploads', { path: './uploads', etag: false, cacheControl: 0 }) router.static('/docs', { path: './docs/build', etag: true, cacheControl: 3600 }) await router.serve(8000) ``` ## Common Patterns ![One request picks the static prefix it starts with, so GET slash uploads slash img slash a dot png matches the slash uploads pattern, has its prefix sliced off, and is served from the uploads folder with that prefix etag off and no cache, while the same tail under GET slash docs slash img slash a dot png matches the slash docs pattern instead and is served from docs slash build with etag on and a one hour cache, proving the matched prefix decides both folder and cache policy](/diagrams/static-prefix-dispatch.png) ### Website + Admin Panel ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Main website router.static('/', { path: './public', etag: true, cacheControl: 86400 }) // Admin panel router.static('/admin', { path: './admin/dist', etag: true, cacheControl: 86400 }) ``` ### Assets + Uploads ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Static assets with long-term caching router.static('/assets', { path: './public/assets', etag: true, cacheControl: 31536000 // 1 year }) // User uploads without caching router.static('/uploads', { path: './uploads', etag: false, cacheControl: 0 // No cache }) ``` ### Development + Production ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Development files - short cache router.static('/dev', { path: './dev', etag: true, cacheControl: 0 // No cache for dev }) // Production build - long cache router.static('/', { path: './dist', etag: true, cacheControl: 31536000 // 1 year }) ``` ## Directory Structure Examples ### Full-Stack Application ``` . ├── main.ts ├── public/ │ ├── index.html │ ├── css/ │ └── js/ ├── admin/ │ └── dist/ │ ├── index.html │ └── assets/ ├── uploads/ │ ├── images/ │ └── documents/ └── docs/ └── build/ ├── index.html └── assets/ ``` ### Microservices Frontend ``` . ├── main.ts ├── web/ │ └── dist/ ├── api/ │ └── docs/ ├── admin/ │ └── build/ └── mobile/ └── public/ ``` ## Configuration Examples ### Different Caching Strategies ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Long-term cached assets (1 year) router.static('/assets', { path: './public/assets', etag: true, cacheControl: 31536000 }) // Medium-term cache (1 day) router.static('/images', { path: './public/images', etag: true, cacheControl: 86400 }) // No caching for dynamic uploads router.static('/uploads', { path: './uploads', etag: false, cacheControl: 0 }) ``` ### Different ETag Settings ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Enable ETag for efficient caching router.static('/static', { path: './public', etag: true, cacheControl: 86400 }) // Disable ETag for frequently changing files router.static('/reports', { path: './reports', etag: false, cacheControl: 3600 }) ``` ## Troubleshooting ### Route Conflicts Routes are registered for all HTTP methods (`GET`, `POST`, etc.). Make sure static routes don't conflict with dynamic routes: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.static('/', { path: './public' }) router.static('/admin', { path: './admin/dist' }) ``` ### File Not Found * Check `path` values are correct (relative to cwd or absolute) * Verify directory structure matches configuration * Ensure files exist in the specified directories * Check URL paths match the route pattern ### Performance Issues * Enable `etag: true` for efficient caching * Set appropriate `cacheControl` values based on content type * Static assets: long cache (31536000 = 1 year) * Dynamic content: short or no cache (0 or 3600) --- --- url: 'https://docs-deserve.neabyte.com/middleware/observability/overview.md' description: >- Overview of Deserve observability: lifecycle events, logging, and error reporting. --- # Observability Overview Deserve emits lifecycle and error events through a built-in event bus. A single `router.on()` subscription receives every event, which keeps logging, metrics, and error reporting in one place instead of scattering `console.log` calls across handlers. This middleware-style hook sits beside the router and watches everything that happens, from server startup to each finished request. ![Server, route, view, request, and process signals all funnel into a single event bus that fans every event to one router.on listener, where you filter by event kind, and the emit is a no-op while no listener is registered](/diagrams/obs-single-bus.png) ## Subscribing to Events `router.on()` registers a listener and returns an unsubscribe function: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Receive every lifecycle and error event const off = router.on((event) => { console.log(event.kind, event.metadata) }) await router.serve(8000) // Stop listening later off() ``` The listener fires for all event kinds, so filtering happens inside the callback. ## Event Shape Every event shares the same envelope: ```typescript { type: 'internal' | 'external', // origin channel kind: string, // event name, such as 'request:complete' metadata: { ... }, // fields specific to the kind timestamp: number // epoch milliseconds } ``` * **`type`** - `external` for normal client traffic, `internal` for framework faults and timeouts. A request event is `internal` when a framework error or the synthetic 503 timeout produced it, otherwise `external`. Every other kind is always `internal`. * **`kind`** - the discriminant used to tell events apart. * **`metadata`** - readonly fields that depend on the kind. * **`timestamp`** - when the event was created. The full list of kinds and their metadata lives in [Event Reference](/middleware/observability/events). ## Difference From a Domain Event Bus The observability bus reports framework activity such as requests, routes, views, and faults. A domain event bus carries application facts like `user:created`. They serve different jobs and often run side by side. See the [domain event bus pattern](/core-concepts/multi-service#event-bus) for sharing application events across services. ## Where to Go Next * [Event Reference](/middleware/observability/events) - every event kind and its metadata. * [Request Logging](/middleware/observability/logging) - turn events into a structured access log. * [Error Reporting](/middleware/observability/errors) - record failures and pair with [error handling](/error-handling/object-details). --- --- url: 'https://docs-deserve.neabyte.com/rendering/performance.md' description: >- Performance characteristics and caching behavior of the Deserve template engine. --- # Performance and Limits The DVE engine caches compiled templates and guards rendering with two limits, so large pages stay fast and a runaway template fails loudly instead of hanging the server. ## Caching Templates are compiled once, then the parsed AST is reused on every later render: ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare const ctx: Context declare const data: DataRecord declare const newData: DataRecord // ---cut--- // First render compiles and caches AST await ctx.render('template', data) // Later renders reuse cached AST await ctx.render('template', newData) ``` The cache covers template compilation only, not data or backend logic. A change to the file clears its cache entry through [hot reload](/core-concepts/hot-reload). ## Iteration Limit Each {{#each}} block is capped at `100_000` iterations by default, which prevents event loop starvation from an unbounded loop. Tune it with `maxIterations`: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ viewsDir: './views', maxIterations: 200_000 }) ``` When a loop exceeds the limit, the engine throws and the server responds with **500**. For very large datasets, reach for [streaming rendering](/rendering/streaming). For CPU-heavy rendering, offload to a [worker pool](/core-concepts/worker-pool). ## Include Depth Limit Template includes are capped at 64 levels of nesting, so a circular or runaway include chain throws an error instead of looping forever. Keeping partials shallow stays well within this limit. --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/philosophy.md' description: >- The design philosophy behind Deserve: convention over configuration, zero dependencies, and Deno-native ergonomics. --- # Philosophy Building a server should feel light, not like solving a puzzle before the first route even runs. That feeling is the reason Deserve exists. ## The Journey Like many developers, I spent years across the JavaScript ecosystem, jumping between frameworks for every new idea. [Express](https://github.com/expressjs/express) was my home base, simple and familiar, and I shipped countless projects on it. Then Deno arrived, and something clicked. Deno gives you a rich native runtime, yet rich can quietly turn into heavy. Config files in one corner, route registrations in another, middleware wiring scattered everywhere. I wanted a way to build on Deno that stayed as small as the problem in front of me, so Deserve started as the framework I wished already existed. ## Core Beliefs These four beliefs shape every decision in the framework, and each one connects to a feature you can reach today. ![Each of the four core beliefs maps to a concrete feature you can reach today, where fewer moving parts leads to zero dependency, structure is the API leads to file-based routing, build on the platform leads to native HTTP and streams, and experience that scales leads to built for teams](/diagrams/philosophy-beliefs-to-features.png) ![An abstract view of how the beliefs think as one mind, where a single root idea of staying as small as the problem feeds all four beliefs, the beliefs reinforce one another down the chain, and together they converge on the conclusion that simple is safe because less code means less that can go wrong](/diagrams/philosophy-principle-web.png) ### Fewer Moving Parts The smallest dependency tree is the one that cannot break. Deno already ships request handling, file watching, and security primitives, so leaning on the runtime beats pulling another package. That is why Deserve runs with [zero npm dependencies](/core-concepts/zero-dependency), keeping the surface small enough to actually trust. ### Structure Is the API A folder layout already describes intent, so it should be the routing too. No registration step, no central table to keep in sync, just files that map straight to URLs through [file-based routing](/core-concepts/file-based-routing). The shape of the project is the shape of the API. ### Build on the Platform When the runtime hands you something solid, use it instead of rebuilding it. Deserve wraps Deno's native HTTP, streams, and workers rather than hiding them, so the platform stays close and predictable underneath every handler. ### Experience That Scales Code should read cleanly, patterns should stay predictable, and errors should point somewhere useful. That care holds whether one person is hacking on a weekend or a whole team is shipping together, which is what makes Deserve [built for teams](/getting-started/built-for-teams) from the first commit. ## Safe by Default Simple and safe belong in the same sentence. A serving router protects the process from accidental shutdown through [process protection](/getting-started/server-configuration#process-protection), and faults are caught in layers through [defense in depth](/error-handling/defense-in-depth). Staying small is part of staying safe, since less code means less that can go wrong. ## Small on Purpose Deserve is not here to replace the big frameworks or win a benchmark war. It is a tool for developers who love how light Deno feels and want to keep that feeling all the way to production. Sometimes the best solution is the simple one. Sometimes that simple solution does not exist yet, so it is worth building one and sharing it openly. --- --- url: 'https://docs-deserve.neabyte.com/getting-started/quick-start.md' description: Build your first Deserve HTTP server and route in under five minutes. --- # Quick Start Get a Deserve server running in under 5 minutes. ## Project Structure This guide ends with the following project structure: ``` . ├── main.ts └── routes/ └── index.ts ``` ## 1. Create the Server Create `main.ts`: ```typescript twoslash import { Router } from '@neabyte/deserve' // Router defaults routesDir to ./routes const router = new Router() // Listen on port 8000 await router.serve(8000) ``` ## 2. Create the First Route Create a `routes` folder and add `index.ts`: ```typescript twoslash import type { Context } from '@neabyte/deserve' // GET handler maps to this route export function GET(ctx: Context): Response { // Reply with a JSON body return ctx.send.json({ message: 'Hello from Deserve!', timestamp: new Date().toISOString() }) } ``` ## 3. Run the Server ```bash deno run --allow-net --allow-read main.ts ``` ## 4. Test the API ```bash curl http://localhost:8000 ``` The response looks like this: ```json { "message": "Hello from Deserve!", "timestamp": "2077-01-01T00:00:00.000Z" } ``` --- --- url: 'https://docs-deserve.neabyte.com/response/redirect.md' description: >- Create redirect responses with ctx.send.redirect(), including allowed status codes and safety rules. --- # Redirect Responses The `ctx.send.redirect()` method creates a redirect response to another URL. The default status is 302 (temporary redirect) and the accepted statuses are 301 (permanent), 302, 303 (see other), 307 (temporary) and 308 (permanent), so any other status throws `Deno.errors.InvalidData`. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Defaults to a 302 redirect return ctx.send.redirect('https://example.com') } ``` ## With Custom Status Code ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // Permanent redirect (301) export function permanent(ctx: Context): Response { return ctx.send.redirect('https://example.com', 301) } // Temporary redirect (302), the default export function temporary(ctx: Context): Response { return ctx.send.redirect('https://example.com', 302) } // See Other (303) export function seeOther(ctx: Context): Response { return ctx.send.redirect('https://example.com', 303) } // Temporary, keep method (307) export function keepTemporary(ctx: Context): Response { return ctx.send.redirect('https://example.com', 307) } // Permanent, keep method (308) export function keepPermanent(ctx: Context): Response { return ctx.send.redirect('https://example.com', 308) } ``` ## With Custom Headers The third argument carries extra response headers: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Redirect 302 with one extra header return ctx.send.redirect('/dashboard', 302, { headers: { 'X-Redirect-Reason': 'login' } }) } ``` ## URL Resolution A relative target resolves against the current request URL and must stay on the same origin, which guards against open redirects. To send a visitor to another site, pass a full `https://` URL on purpose: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Same-origin relative path, resolved safely return ctx.send.redirect('/login') } ``` The target must use the `http` or `https` scheme. A relative path that resolves to a different origin, a non-http scheme, or an unparseable URL throws `Deno.errors.InvalidData`. Any `Location` passed through the headers is ignored, since the resolved URL always wins. ## Method Signature ```typescript ctx.send.redirect( url: string, status?: 301 | 302 | 303 | 307 | 308, options?: { headers?: HeadersInit } ): Response ``` * **url** - target location for the redirect * **status** - redirect status, defaults to `302` * **options** - optional response headers --- --- url: 'https://docs-deserve.neabyte.com/rendering.md' description: Server-side template rendering in Deserve using the built-in DVE view engine. --- # Rendering Overview > See [DVE syntax highlighting](https://github.com/NeaByteLab/Deserve/tree/main/editor) documentation. Deserve ships a built-in template engine called DVE (Deserve View Engine) for building dynamic HTML from plain templates with a small {{ }} syntax. ## Setup Point `viewsDir` at the templates folder when creating the router: ```typescript twoslash import { Router } from '@neabyte/deserve' // Point viewsDir at the templates folder const router = new Router({ viewsDir: './views' }) await router.serve(8000) ``` ## First Template Create a `.dve` file inside the views folder: ```html {{title}}

Hello {{name}}!

Today: {{date}}

``` Then render it from a route with `ctx.render()`: ```typescript twoslash // routes/welcome.ts import type { Context } from '@neabyte/deserve' export async function GET(ctx: Context): Promise { // Render template with data return await ctx.render('welcome', { title: 'Welcome Page', name: 'John Doe', date: new Date().toLocaleDateString() }) } ``` The `.dve` extension is optional in the path, so `'welcome'` and `'welcome.dve'` both resolve to the same file. ## Error Handling A missing template throws `Template "" not found in views directory`, and a render fault throws too. Let either reach the [centralized error handler](/error-handling/object-details), or catch it in the handler for a precise reply: ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare const data: DataRecord // ---cut--- export async function GET(ctx: Context): Promise { try { return await ctx.render('template', data) } catch (error) { const message = error instanceof Error ? error.message : '' if (message.includes('not found in views directory')) { return ctx.send.json({ error: 'Template missing' }, { status: 404 }) } return ctx.send.json({ error: 'Render failed' }, { status: 500 }) } } ``` ## Where to Go Next * [Template Syntax](/rendering/syntax) - variables, conditionals, loops, includes, and expressions. * [Performance and Limits](/rendering/performance) - caching, the iteration limit, and the include depth limit. * [Streaming Rendering](/rendering/streaming) - send HTML chunk by chunk for large pages. --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/request-handling.md' description: >- How Deserve parses and handles incoming requests, including body parsing and content negotiation. --- # Request Handling > **Reference**: [Deno Request API Documentation](https://docs.deno.com/deploy/classic/api/runtime-request/) Deserve provides a `Context` object that wraps the native `Request`, so query, route params, headers, cookies, and body all come through Context without manual parsing. For the full Context surface, including response helpers and state, see [Context Object](/core-concepts/context-object). A handler receives one `Context` and reads whatever it needs from it: ```typescript twoslash import type { Context } from '@neabyte/deserve' // Read request data from ctx export function GET(ctx: Context): Response { const query = ctx.query() return ctx.send.json({ query }) } ``` The sections below cover each kind of input, and [Method Reference](#method-reference) lists every reader with its return type. ## Query Parameters Query strings are parsed on first access, then cached. Two readers cover every case, `query()` for a single value and `queries()` for repeated keys: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // URL: /search?q=deno&limit=10 export function GET(ctx: Context): Response { const query = ctx.query() return ctx.send.json({ search: query.q, limit: parseInt(query.limit || '10') }) } ``` When a key repeats in the URL, `query()` keeps the **last value** while `queries()` returns **all of them**: ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // URL: /search?tag=deno&tag=typescript ctx.query('tag') // 'typescript', last value wins ctx.queries('tag') // ['deno', 'typescript'], every value ``` Reach for `queries()` on array or multi-select inputs, and `query()` everywhere else. The full signatures live in [Method Reference](#method-reference). ## Route Parameters Dynamic segments from [file-based routing](/core-concepts/file-based-routing) arrive as route params, read one at a time with `param()` or all at once with `params()`: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // routes/users/[id]/posts/[postId].ts // URL: /users/123/posts/456 export function GET(ctx: Context): Response { const id = ctx.param('id') // '123' const all = ctx.params() // { id: '123', postId: '456' } return ctx.send.json({ id, all }) } ``` Values are percent-decoded once before the handler reads them. How patterns are matched is covered in [Route Patterns](/core-concepts/route-patterns). ## Method Reference ### `ctx.query(key?)` Returns all query parameters as an object, and falls back to the **last value for duplicate keys**. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // URL: /search?q=deno&limit=10 ctx.query() // { q: 'deno', limit: '10' } // URL: /search?tag=deno&tag=typescript ctx.query() // { tag: 'typescript' } ← last value only // Single parameter const q = ctx.query('q') // Returns: 'deno' ``` ### `ctx.queries(key)` Returns **all values** for one query parameter key as an array. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // URL: /search?tags=deno&tags=typescript const tags = ctx.queries('tags') // ['deno', 'typescript'] ← all values // query() covers single or last value, while queries() covers arrays and multi-select ``` ### `ctx.param(key)` Returns a single route parameter value. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Route: /users/[id] // URL: /users/123 const id = ctx.param('id') // '123' ``` ### `ctx.params()` Returns all route parameters as an object. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Route: /users/[id]/posts/[postId] // URL: /users/123/posts/456 const params = ctx.params() // { id: '123', postId: '456' } ``` ### `ctx.body()` Parses the request body automatically as JSON, form-data, or text. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/users with JSON body export async function POST(ctx: Context): Promise { const body = await ctx.body() // { name: 'John', age: 30 } return ctx.send.json({ created: body }) } ``` ### `ctx.json()` Parses the request body as JSON. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/users with JSON body export async function POST(ctx: Context): Promise { const body = await ctx.json() // { name: 'John', age: 30 } return ctx.send.json({ created: body }) } ``` ### `ctx.formData()` Parses the request body as form data and returns a `FormData` object. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/users with form data export async function POST(ctx: Context): Promise { const formData = await ctx.formData() // FormData object const name = formData.get('name') // 'John' return ctx.send.json({ name }) } ``` ### `ctx.text()` Reads the request body as raw text. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/text with plain text export async function POST(ctx: Context): Promise { const text = await ctx.text() // 'Hello World' return ctx.send.text(text) } ``` ### `ctx.arrayBuffer()` Reads the request body as an ArrayBuffer, which suits binary data processing. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/upload with binary data export async function POST(ctx: Context): Promise { const buffer = await ctx.arrayBuffer() // ArrayBuffer object // Process binary data... return ctx.send.json({ size: buffer.byteLength }) } ``` ### `ctx.blob()` Reads the request body as a Blob, which suits file uploads and binary handling. ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- // POST /api/upload with file data export async function POST(ctx: Context): Promise { const blob = await ctx.blob() // Blob object // Process file data... return ctx.send.json({ type: blob.type, size: blob.size }) } ``` ### `ctx.header(key?)` Reads one header by key or every header at once, matching keys case-insensitively and lowercasing them. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Get specific header const contentType = ctx.header('content-type') // Get all headers as object const headers = ctx.header() ``` ### `ctx.headers` Exposes the raw Headers object for direct access. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Access raw Headers API const contentType = ctx.headers.get('Content-Type') ``` ### `ctx.cookie(key?)` Reads one cookie by key or every cookie at once. ```typescript twoslash import type { Context } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Get specific cookie const sessionId = ctx.cookie('sessionId') // Get all cookies const cookies = ctx.cookie() // { sessionId: 'abc123', theme: 'dark' } ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/observability/logging.md' description: Turn Deserve request events into structured request logs. --- # Request Logging A single [`router.on()`](/middleware/observability/overview) subscription turns every finished request into a structured access log, with no logging code inside handlers. ![Every finished request emits request:complete with OpenTelemetry-aligned metrics, and a request with status 400 or higher also emits request:error carrying the original error, so one router.on listener fans the same envelope into an access log line, a slow request warning filtered by duration, and an error report](/diagrams/obs-request-lifecycle.png) ## Basic Access Log Listen for `request:complete` and print one line per request: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // One log line per finished request router.on((event) => { if (event.kind === 'request:complete') { const { method, url, statusCode, durationMs } = event.metadata as { method: string url: string statusCode: number durationMs: number } console.log(`${method} ${url} ${statusCode} ${Math.round(durationMs)}ms`) } }) await router.serve(8000) ``` ## Structured JSON Logs Emit JSON when a log pipeline expects structured records: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // ---cut--- router.on((event) => { if (event.kind === 'request:complete') { // Forward the full metadata as JSON console.log(JSON.stringify({ at: event.timestamp, ...event.metadata })) } }) ``` The metadata already includes OpenTelemetry-aligned fields like `route`, `serverAddress`, `userAgent`, and `requestSize`. See the [Event Reference](/middleware/observability/events#requests) for the full list. ## Logging Slow Requests Filter by duration to surface only slow traffic: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // ---cut--- router.on((event) => { // Flag requests slower than 500ms if (event.kind === 'request:complete') { const { url, durationMs } = event.metadata as { url: string; durationMs: number } if (durationMs > 500) { console.warn(`SLOW ${url} ${Math.round(durationMs)}ms`) } } }) ``` For failures, see [Error Reporting](/middleware/observability/errors). --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/route-patterns.md' description: >- Route pattern syntax in Deserve including dynamic params, wildcards, and matching rules. --- # Route Patterns > **Reference**: [Fast Router GitHub Repository](https://github.com/NeaByteLab/Fast-Router) [File-based Routing](/core-concepts/file-based-routing) covers the rules that turn a folder into URLs. This page covers the other half, the matching engine that decides which file answers an incoming request. Deserve uses **Fast Router** (radix tree) to match a path and pull out parameters, where a `[param]` folder becomes a `:param` slot at the router level. ## Pattern Matching Deserve converts file paths to route patterns, and **FastRouter** matches them with a radix tree for fast lookups: ``` . ├── routes/index.ts → / ├── routes/about.ts → /about ├── routes/users/[id].ts → /users/:id ├── routes/users/[id]/posts.ts → /users/:id/posts ``` ## How Matching Works When a request arrives, the engine looks up the method and pathname, then applies a few fixed rules: * **Exact path, exact method** - the matching handler runs with its params filled in * **HEAD falls back to GET** - a `HEAD` with no handler reuses the `GET` handler * **Wrong method** - a known path with no handler for that method returns **405** with an `Allow` header listing the methods that do exist * **Unknown path** - no match returns **404** through the [error handler](/error-handling/object-details) * **Oversized input** - a URL past `maxUrlLength` or a param past `maxParamLength` returns **414**, both tunable in [Server Configuration](/getting-started/server-configuration) Params are percent-decoded once before the handler reads them, so `ctx.param('id')` returns the decoded value. ## Dynamic Parameters A `[param]` folder or file becomes a named `:param` slot in the pattern. Each bracket in the path turns into one parameter, and nesting just adds more: | File path | Pattern | Params | | -------------------------------------------------- | ------------------------------------------ | ---------------------------- | | `users/[id].ts` | `/users/:id` | `id` | | `users/[id]/posts/[postId].ts` | `/users/:id/posts/:postId` | `id`, `postId` | | `api/v1/users/[userId]/posts/[postId].ts` | `/api/v1/users/:userId/posts/:postId` | `userId`, `postId` | The matched values are read inside a handler with `ctx.param()` and `ctx.params()`, covered in [Request Handling](/core-concepts/request-handling#route-parameters). ## Pattern Examples ### User Management ``` routes/ ├── users.ts → /users ├── users/[id].ts → /users/:id ├── users/[id]/profile.ts → /users/:id/profile ├── users/[id]/posts.ts → /users/:id/posts └── users/[id]/posts/[postId].ts → /users/:id/posts/:postId ``` ### API Versioning ``` routes/ ├── api/ │ ├── v1/ │ │ └── users/[id].ts → /api/v1/users/:id │ └── v2/ │ └── users/[id].ts → /api/v2/users/:id ``` ### Blog System ``` routes/ ├── blog/ │ ├── [slug].ts → /blog/:slug │ └── [year]/ │ └── [month]/ │ └── [day]/ │ └── [slug].ts → /blog/:year/:month/:day/:slug ``` ## Parameter Validation The router matches the shape of a pattern, not the meaning of a value, so `/users/:id` matches `abc` just as happily as `123`. A handler validates the value and returns a status code that flows to the [error handler](/error-handling/object-details): ```typescript twoslash // File: routes/users/[id].ts import type { Context } from '@neabyte/deserve' // Reject non-numeric ids with 400 export function GET(ctx: Context): Response { const id = ctx.param('id') if (!id || !/^\d+$/.test(id)) { return ctx.send.json({ error: 'Invalid user ID' }, { status: 400 }) } return ctx.send.json({ userId: parseInt(id) }) } ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/route-specific.md' description: Scope middleware to a path prefix so it runs only for matching routes. --- # Route-Specific Middleware Route-specific middleware applies to specific route patterns, allowing targeted functionality like authentication for API routes or logging for admin routes. Matching is boundary-aware: `router.use('/api', fn)` runs for `/api` and `/api/users`, but not for `/apiv2`, because the pathname must equal the prefix or continue with a `/`. ![Route-Specific prefix matching: router.use('/api', fn) matches /api exactly and /api/users on a boundary slash, but skips /apiv2 and /admin because they are not boundary matches of the prefix](/diagrams/middleware-route-matching.png) ## Basic Usage Apply middleware to specific route patterns using the `use()` method with a route path: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // Runs for paths starting with /api router.use('/api', async (ctx, next) => { console.log(`API request: ${ctx.request.method} ${ctx.url}`) return await next() }) await router.serve(8000) ``` ## Route Pattern Matching Middleware applies to routes that start with the specified pattern: ```typescript twoslash import type { MiddlewareFn } from '@neabyte/deserve' import { Router } from '@neabyte/deserve' const router = new Router() declare const middleware: MiddlewareFn // ---cut--- // Applies to /api/* routes router.use('/api', middleware) // Applies to /api/users/* routes router.use('/api/users', middleware) // Applies to /admin/* routes router.use('/admin', middleware) ``` ## Common Route-Specific Patterns ### API Authentication ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() declare function isValidToken(token: string): boolean // ---cut--- // Require a bearer token under /api router.use('/api', async (ctx, next) => { const authHeader = ctx.header('authorization') if (!authHeader) { return ctx.send.text('API requires authentication', { status: 401 }) } const token = authHeader.replace('Bearer ', '') if (!isValidToken(token)) { return ctx.send.text('Invalid token', { status: 401 }) } return await next() }) ``` ### Admin Authorization ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Allow only the admin role under /admin router.use('/admin', async (ctx, next) => { const userRole = ctx.header('x-user-role') if (userRole !== 'admin') { return ctx.send.text('Admin access required', { status: 403 }) } return await next() }) ``` ### Public Route Logging ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Log access under /public router.use('/public', async (ctx, next) => { console.log(`Public access: ${ctx.request.method} ${ctx.url}`) return await next() }) ``` ### Version-Specific Middleware ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Separate middleware per API version router.use('/api/v1', async (ctx, next) => { console.log('Legacy API v1 request') return await next() }) router.use('/api/v2', async (ctx, next) => { console.log('Modern API v2 request') return await next() }) ``` ## Multiple Route-Specific Middleware Apply multiple middleware to the same route pattern: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Auth runs first under /api router.use('/api', async (ctx, next) => { const authHeader = ctx.header('authorization') if (!authHeader) { return ctx.send.text('Unauthorized', { status: 401 }) } return await next() }) // Logging runs after auth passes router.use('/api', async (ctx, next) => { console.log(`API: ${ctx.request.method} ${ctx.url}`) return await next() }) ``` ## Nested Route Patterns Apply middleware to nested route patterns: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Covers every path under /api router.use('/api', async (ctx, next) => { console.log('API request') return await next() }) // Narrows to /api/users router.use('/api/users', async (ctx, next) => { console.log('User API request') return await next() }) // Narrows further and checks role router.use('/api/users/admin', async (ctx, next) => { const role = ctx.header('x-user-role') if (role !== 'admin') { return ctx.send.text('Admin access required', { status: 403 }) } return await next() }) ``` ## Middleware Execution Order Middleware runs in the order it is added: ![Route-Specific execution for GET /api/users in one chain: the global logger runs, the /api auth runs on a prefix match, the /admin guard is skipped because it does not match without consuming a turn, the /api/users logger runs, then the route handler executes, all in registration order](/diagrams/middleware-route-chain.png) ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Global runs for every request router.use(async (ctx, next) => { console.log('Global middleware') return await next() }) // Path middleware runs for /api requests router.use('/api', async (ctx, next) => { console.log('API middleware') return await next() }) // For /api/users: global, then API, then handler ``` --- --- url: 'https://docs-deserve.neabyte.com/getting-started/routes-configuration.md' description: >- Configure the routes directory, parameter limits, and request timeouts in the Deserve Router. --- # Routes Configuration Configure the Deserve routes directory to match the project structure. ## Router Options The `Router` constructor accepts configuration options. The common ones are `routesDir` for the route folder and `requestTimeoutMs` for a request timeout. Rendering, request limits, worker pools, and a custom error builder are all configurable too. Proxy trust through `trustProxy` and the worker pool live in [Client IP Resolution](/getting-started/server-configuration#client-ip-resolution) and [Worker Pool](/core-concepts/worker-pool). ```typescript twoslash import { Router } from '@neabyte/deserve' // Custom routes folder and timeout const router = new Router({ routesDir: 'src/routes', requestTimeoutMs: 30_000 }) ``` ## Configuration Options ### `routesDir` The directory containing the route files: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- // Defaults to ./routes const defaultRouter = new Router() // Read routes from ./src/api const router = new Router({ routesDir: 'src/api' }) ``` ### `requestTimeoutMs` Optional timeout in milliseconds for the full request (middleware + route handler). If exceeded, the server responds with **503 Service Unavailable**. Omit or leave undefined for no timeout. ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes', requestTimeoutMs: 30_000 }) ``` ### `maxIterations` Maximum iterations allowed per {{#each}} block in DVE templates. The cap prevents event loop starvation from unbounded rendering. The default is `100_000`, and exceeding it makes the engine throw so the server responds with **500 Internal Server Error**. ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes', viewsDir: './views', maxIterations: 50_000 }) ``` For datasets larger than the limit, use [`streamRender`](/rendering/streaming) instead, and see [Performance and Limits](/rendering/performance#iteration-limit) for how the cap behaves. For CPU-intensive rendering, consider offloading to a [worker pool](/core-concepts/worker-pool). ### `maxUrlLength` Maximum length of the request URL in characters. A longer URL is rejected with **414 URI Too Long** before any route runs. The default is `8192`: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes', maxUrlLength: 4096 }) ``` ### `maxParamLength` Maximum length of a single route parameter value. A longer value is rejected with **414 URI Too Long**. The default is `1024`: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes', maxParamLength: 512 }) ``` ### `errorResponseBuilder` Advanced option that replaces how error responses are built. It receives the context, status code, error, and the handler set with [`router.catch()`](/error-handling/object-details), and returns the final `Response`. Most apps shape errors through `router.catch()` instead, covered in [Error Handling](/error-handling/object-details): ```typescript twoslash import type { Context, ErrorMiddleware } from '@neabyte/deserve' import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes', errorResponseBuilder: { // Build a custom error response async build( ctx: Context, statusCode: number, error: Error, errorMiddleware?: ErrorMiddleware ) { return ctx.send.json( { failed: true, statusCode }, { status: statusCode } ) } } }) ``` ## Supported File Extensions Deserve automatically detects and supports these file extensions: * `.ts` (TypeScript) * `.js` (JavaScript) * `.tsx` (TypeScript with JSX) * `.jsx` (JavaScript with JSX) * `.mjs` (ES Modules) * `.cjs` (CommonJS) No extra configuration is needed, since Deserve detects them automatically. ## Absolute vs Relative Paths ### Relative Paths ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: 'routes' }) ``` ### Absolute Paths ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: `${Deno.cwd()}/routes` }) ``` ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ routesDir: '/absolute/path/to/routes' }) ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/security-headers.md' description: >- Apply common security response headers with the Deserve security headers middleware. --- # Security Headers Middleware > **Reference**: [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) Security Headers middleware sets HTTP security headers that protect the application from common vulnerabilities like clickjacking, MIME type sniffing, and XSS attacks. It is secure by default, so calling it with no options already applies a strong baseline. ## Basic Usage Calling `Mware.securityHeaders()` with no options applies the secure defaults: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Apply secure default headers router.use(Mware.securityHeaders()) await router.serve(8000) ``` The defaults set these headers on every response: | Header | Default value | | ----------------------------------- | -------------- | | `Cross-Origin-Opener-Policy` | `same-origin` | | `Cross-Origin-Resource-Policy` | `same-origin` | | `Origin-Agent-Cluster` | `?1` | | `Referrer-Policy` | `no-referrer` | | `X-Content-Type-Options` | `nosniff` | | `X-DNS-Prefetch-Control` | `off` | | `X-Download-Options` | `noopen` | | `X-Frame-Options` | `SAMEORIGIN` | | `X-Permitted-Cross-Domain-Policies` | `none` | Pass options to override a default or to enable headers that stay off until configured: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Override defaults where needed router.use( Mware.securityHeaders({ xFrameOptions: 'DENY', strictTransportSecurity: 'max-age=31536000; includeSubDomains' }) ) ``` ## Route-Specific Security Headers Apply different security headers to specific routes: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Strict headers for admin routes router.use( '/admin', Mware.securityHeaders({ xContentTypeOptions: 'nosniff', xFrameOptions: 'DENY', referrerPolicy: 'no-referrer', strictTransportSecurity: 'max-age=31536000; includeSubDomains' }) ) // Less strict for public routes router.use( '/api/public', Mware.securityHeaders({ xContentTypeOptions: 'nosniff', xFrameOptions: 'SAMEORIGIN' }) ) ``` ## Configuration Options Each header option takes three forms. A string value sets the header to that value. `false` omits the header, even one that has a secure default. Leaving an option `undefined` keeps its default when it has one, or skips it otherwise. The four headers without a default - `contentSecurityPolicy`, `crossOriginEmbedderPolicy`, `strictTransportSecurity`, and `xPoweredBy` - stay off until a value is given. ### `contentSecurityPolicy` Content Security Policy (CSP) to control resource loading: ```typescript contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'" ``` ### `crossOriginEmbedderPolicy` Cross-Origin Embedder Policy (COEP): ```typescript crossOriginEmbedderPolicy: 'require-corp' // or 'unsafe-none', 'credentialless' ``` ### `crossOriginOpenerPolicy` Cross-Origin Opener Policy (COOP): ```typescript crossOriginOpenerPolicy: 'same-origin' // or 'same-origin-allow-popups', 'unsafe-none' ``` ### `crossOriginResourcePolicy` Cross-Origin Resource Policy (CORP): ```typescript crossOriginResourcePolicy: 'same-origin' // or 'same-site', 'cross-origin' ``` ### `originAgentCluster` Origin Agent Cluster isolation: ```typescript originAgentCluster: '?1' ``` ### `referrerPolicy` Referrer Policy to control referrer information: ```typescript referrerPolicy: 'no-referrer' // or 'strict-origin-when-cross-origin', etc. ``` ### `strictTransportSecurity` HTTP Strict Transport Security (HSTS): ```typescript strictTransportSecurity: 'max-age=31536000; includeSubDomains' ``` ### `xContentTypeOptions` Prevents MIME type sniffing: ```typescript xContentTypeOptions: 'nosniff' ``` ### `xDnsPrefetchControl` Controls DNS prefetching: ```typescript xDnsPrefetchControl: 'off' // or 'on' ``` ### `xDownloadOptions` Controls file download options: ```typescript xDownloadOptions: 'noopen' ``` ### `xFrameOptions` Prevents clickjacking attacks: ```typescript xFrameOptions: 'DENY' // or 'SAMEORIGIN', 'ALLOW-FROM uri' ``` ### `xPermittedCrossDomainPolicies` Cross-domain policy for Flash: ```typescript xPermittedCrossDomainPolicies: 'none' // or 'master-only', 'all' ``` ### `xPoweredBy` Off by default. Set a string to advertise a value, or leave it for no header: ```typescript xPoweredBy: 'Custom' // Add a custom value ``` ## Complete Example ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) // Apply a broad set of headers router.use( Mware.securityHeaders({ xContentTypeOptions: 'nosniff', xFrameOptions: 'DENY', referrerPolicy: 'no-referrer', xDnsPrefetchControl: 'off', strictTransportSecurity: 'max-age=31536000; includeSubDomains', contentSecurityPolicy: "default-src 'self'", crossOriginOpenerPolicy: 'same-origin', crossOriginResourcePolicy: 'same-origin' }) ) await router.serve(8000) ``` ## Important Notes * **Secure by default**: calling the middleware with no options already applies nine baseline headers * **String value**: sets the header to that exact value, overriding any default * **Set to `false`**: omits the header, even one that has a default * **Undefined**: keeps the default when the header has one, otherwise skips it * **X-Powered-By**: off by default, set a string to add it or leave it for no header * **HSTS**: apply `strictTransportSecurity` only on HTTPS servers * **CSP**: Content Security Policy can grow complex, so test it thoroughly --- --- url: 'https://docs-deserve.neabyte.com/getting-started/server-configuration.md' description: >- Configure how the Deserve server listens, shuts down gracefully, and protects the process. --- # Server Configuration > **Reference**: [Deno.serve API Documentation](https://docs.deno.com/api/deno/~/Deno.serve) Configure a Deserve server with hostname binding and graceful shutdown. ## Basic Server Setup The simplest way to start a server: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // Bind 0.0.0.0 on port 8000 await router.serve(8000) ``` This starts the server on `0.0.0.0:8000`, which covers all interfaces. ## Enhanced Serve Method Deserve's enhanced `serve` method supports three parameters: ```typescript // Method signatures async serve(port?: number): Promise async serve(port?: number, hostname?: string): Promise async serve(port?: number, hostname?: string, signal?: AbortSignal): Promise ``` ## Hostname Binding ### Bind to Specific Interface ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Bind to localhost only await router.serve(8000, '127.0.0.1') // Bind to all interfaces (default) await router.serve(8000, '0.0.0.0') // Bind to specific network interface await router.serve(8000, '192.168.1.100') ``` ### Development vs Production ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Development - localhost only await router.serve(8000, '127.0.0.1') // Production - all interfaces await router.serve(8000, '0.0.0.0') ``` ## Request Timeout A request timeout is set when creating the router. When middleware and the route handler do not finish within that time, the server responds with **503 Service Unavailable**: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ requestTimeoutMs: 30_000 }) await router.serve(8000) ``` Omit `requestTimeoutMs` for no timeout (default). ## Template Iteration Limit The `maxIterations` option caps the iterations per {{#each}} block in DVE templates, which prevents event loop starvation from unbounded rendering. The default is `100_000`: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ viewsDir: './views', maxIterations: 50_000 }) await router.serve(8000) ``` If a template exceeds the limit, the server responds with **500 Internal Server Error**. The full rendering behavior lives in [Performance and Limits](/rendering/performance#iteration-limit). For large datasets, use [`streamRender`](/rendering/streaming) instead. For CPU-intensive rendering, consider offloading to a [worker pool](/core-concepts/worker-pool). ## Client IP Resolution The `trustProxy` option controls how the real client IP is resolved when the server runs behind a proxy or load balancer. Without it, `ctx.ip` returns the direct TCP peer: ```typescript twoslash import { Router } from '@neabyte/deserve' // ---cut--- const router = new Router({ trustProxy: [ 'loopback', '10.0.0.0/8' ] }) await router.serve(8000) ``` When the direct peer matches a trusted rule, Deserve reads the forwarded headers to find the real visitor IP. It checks `CF-Connecting-IP` and `X-Real-IP` first, then walks the `X-Forwarded-For` and RFC 7239 `Forwarded` chain from right to left through trusted hops. `trustProxy` accepts these values: * **Preset names** - `'loopback'`, `'linklocal'`, `'uniquelocal'` * **Exact IPs or CIDR ranges** - for example `'10.0.0.0/8'` * **A predicate** - `(ip: string) => boolean` The resolved IP is available on the request context: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Real visitor IP after trustProxy const client = ctx.ip // Direct TCP peer, ignores forwarded headers const peer = ctx.directIp return ctx.send.json({ client, peer }) } ``` Without a matching `trustProxy` rule, `ctx.ip` and `ctx.directIp` return the same direct peer address. The [IP restriction middleware](/middleware/ip) uses `ctx.ip` for its allow and deny rules. ## Graceful Shutdown An `AbortSignal` drives graceful server shutdown: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() const ac = new AbortController() await router.serve(8000, '127.0.0.1', ac.signal) ac.abort() ``` ### Process Signal Handling Without an `AbortSignal`, the router listens for `SIGINT` and `SIGTERM` itself (only `SIGINT` on Windows) and drains gracefully on either one. No manual signal wiring is needed: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // SIGINT and SIGTERM drain automatically await router.serve(8000, '127.0.0.1') ``` Pass an `AbortSignal` when shutdown needs to be driven from code instead of a signal, as shown above. Note that `Deno.exit()` and other termination calls are blocked while the server runs, so lean on `AbortController` or the built-in signal handling rather than exiting by hand. See [Process Protection](#process-protection) for the reason behind this. ## Process Protection A serving router installs a process sentinel that keeps the service alive through faults that would normally take it down. This matters because Deserve runs many things in one process - [hot reload](/core-concepts/multi-service#hot-reload) watchers, [worker pools](/core-concepts/worker-pool), and often several [services side by side](/core-concepts/multi-service). One dependency calling `Deno.exit()` should not drop every service at once. ### What Is Blocked While the server runs, these termination calls are intercepted and turned into a no-op: * `Deno.exit()` and `Deno.kill()` aimed at the current process * `process.exit()`, `process.abort()`, `process.reallyExit()`, and `process.kill()` aimed at the current process A `kill` aimed at another PID still passes through, so only self-termination is blocked. The sentinel is removed once the server stops, which restores normal behavior. ### Not Silent Every blocked call is reported, never swallowed in silence. The sentinel emits a [`process:error`](/middleware/observability/events#process) event with `origin: 'process:exit'` and a message naming the blocked call, for example `Blocked Deno.exit(0) - process termination is not permitted from application code`. Unhandled rejections and uncaught errors surface the same way with `origin: 'unhandledrejection'` or `'uncaughterror'`. Subscribe to see them: ```typescript twoslash import { Router } from '@neabyte/deserve' const router = new Router() // ---cut--- router.on((event) => { if (event.kind === 'process:error') { const { origin, error } = event.metadata as { origin: string; error: Error } // Logs the blocked or uncaught fault console.error(`[${origin}]`, error.message) } }) ``` See [Error Reporting](/middleware/observability/errors) for the full pattern. ### Threat Model The goal is availability. A single faulty or hostile code path should not be able to abort the whole process and deny service to every route and service it hosts. * **Supply chain abuse** - a transitive dependency that calls `process.exit()` or `Deno.exit()`, whether by accident or as an attack, can no longer crash the server. This aligns with [OWASP A03:2025 Software Supply Chain Failures](https://owasp.org/Top10/2025/A03_2025-Software_Supply_Chain_Failures/) and [CWE-1395](https://cwe.mitre.org/data/definitions/1395.html). * **Denial of service** - blocking self-termination removes an easy availability kill switch, related to [CWE-400](https://cwe.mitre.org/data/definitions/400.html) and [CWE-730](https://cwe.mitre.org/data/definitions/730.html). * **Uncaught faults** - trapping unhandled rejections and uncaught errors keeps a single bad request from ending the process, related to [CWE-248](https://cwe.mitre.org/data/definitions/248.html). This is a best-effort defense, not a sandbox. It interposes the known termination entry points rather than isolating untrusted code, so it reduces the blast radius without claiming to stop every possible abuse. Pair it with Deno permission flags and dependency review for stronger guarantees. The layered approach to faults is covered in [Defense in Depth](/error-handling/defense-in-depth). ## Testing Configuration ### Test Basic Server ```bash # Start server deno run --allow-net --allow-read main.ts # Test endpoint curl http://localhost:8000 ``` ### Test Hostname Binding ```bash # Bind to localhost only deno run --allow-net --allow-read main.ts # Should work curl http://127.0.0.1:8000 # Should fail (if binding to 127.0.0.1 only) curl http://0.0.0.0:8000 ``` ### Test Graceful Shutdown ```bash # Start server deno run --allow-net --allow-read main.ts # Send SIGINT (Ctrl+C) # Server should shutdown gracefully ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/session.md' description: Cookie-based session middleware signed with HMAC-SHA256 for per-user state. --- # Session Middleware Session middleware stores session data in a signed cookie and exposes it through framework state, which suits login, preferences, or per-user state without a session database. The cookie payload is signed with HMAC-SHA256, and **`cookieSecret` is required and must be at least 32 characters**. ## Basic Usage `Mware.session({ cookieSecret })` adds a cookie-based session: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // cookieSecret signs the session cookie router.use( Mware.session({ cookieSecret: Deno.env.get('SESSION_SECRET') ?? 'replace-with-secret-min-32-chars' }) ) await router.serve(8000) ``` The middleware stores three values in framework state, read with `ctx.getState`: * **`session`** - session data, an object or `null` when absent or signature invalid * **`setSession`** - async function that saves data and sets the signed cookie * **`clearSession`** - function that clears the session cookie ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare const ctx: Context // ---cut--- // Read session data const session = ctx.getState('session' as never) // Save session data (async) const setSession = ctx.getState<(data: DataRecord) => Promise>('setSession' as never) await setSession?.({ userId: '1' }) // Clear session const clearSession = ctx.getState<() => void>('clearSession' as never) clearSession?.() ``` ## Example: Login And Logout ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' // POST: login, set session when credentials valid export async function POST(ctx: Context): Promise { const body = await ctx.json() as DataRecord // Save a session on matching credentials if (body?.username === 'admin' && body?.password === 'secret') { const setSession = ctx.getState<(data: DataRecord) => Promise>('setSession' as never) await setSession?.({ userId: '1', username: 'admin' }) return ctx.send.json({ ok: true }) } return ctx.send.json({ error: 'Invalid credentials' }, { status: 401 }) } // GET: check login status export function GET(ctx: Context): Response { // Read session from framework state const session = ctx.getState('session' as never) if (!session) { return ctx.send.json({ loggedIn: false }) } return ctx.send.json({ loggedIn: true, user: session }) } // DELETE: logout, clear session export function DELETE(ctx: Context): Response { // Drop the session cookie const clearSession = ctx.getState<() => void>('clearSession' as never) clearSession?.() return ctx.send.json({ ok: true }) } ``` ## Session Options **`cookieSecret`** is required, must be at least 32 characters, and signs the cookie with HMAC-SHA256. The cookie name, max age, path, and security attributes are also configurable: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // ---cut--- // Override the default cookie settings router.use( Mware.session({ cookieSecret: Deno.env.get('SESSION_SECRET') ?? 'fallback-secret-at-least-32-characters', cookieName: 'sid', maxAge: 3600, path: '/', sameSite: 'Lax', httpOnly: true }) ) ``` | Option | Default | Description | | -------------- | ----------- | -------------------------------------------- | | `cookieSecret` | - | **Required, min 32 characters.** Signs the cookie. | | `cookieName` | `'session'` | Cookie name | | `maxAge` | `86400` | Cookie age in seconds (24 hours) | | `path` | `'/'` | Cookie path | | `sameSite` | `'Lax'` | `'Strict' \| 'Lax' \| 'None'` | | `httpOnly` | `true` | Cookie not accessible from JavaScript | | `secure` | `true` | Require HTTPS for the cookie | ### Validation and Expiry The middleware checks its options when created and throws `Deno.errors.InvalidData` when something is unsafe: * `cookieSecret` shorter than 32 characters * `sameSite: 'None'` without `secure: true`, since browsers reject that combination * `maxAge` that is not a positive number, or an empty `path` Each cookie also carries a signed issue time, so the middleware treats a session older than `maxAge` as absent and reads it back as `null`. A tampered cookie fails the signature check and reads as `null` too, which keeps stale or forged sessions from being trusted. ## Limitations * Session data lives in the cookie and is signed with HMAC-SHA256, so it should hold only identifiers or small data rather than large or highly sensitive values. * A server-side or token-based session needs another mechanism such as JWT or Redis outside this middleware. --- --- url: 'https://docs-deserve.neabyte.com/response/stream.md' description: Send streaming responses from a ReadableStream with ctx.send.stream(). --- # Stream Responses The `ctx.send.stream()` method returns a response body from a `ReadableStream`, useful for streaming large data or server-sent events without full buffering. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Push two text chunks then close const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('Hello\n')) controller.enqueue(new TextEncoder().encode('World\n')) controller.close() } }) // Stream becomes the response body return ctx.send.stream(stream) } ``` ## With Custom Content-Type The third parameter is the content type and it defaults to `application/octet-stream`: ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('Hello')) controller.close() } }) // Third arg sets the content type return ctx.send.stream(stream, undefined, 'text/plain') } ``` ## With Status And Headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('{"ok":true}\n')) controller.close() } }) // Second arg status, third arg type return ctx.send.stream(stream, { status: 200, headers: { 'X-Custom': 'value' } }, 'application/x-ndjson') } ``` ## Method Signature ```typescript ctx.send.stream( stream: ReadableStream, options?: ResponseInit, contentType?: string ): Response ``` * **stream** - ReadableStream used as response body * **options** - optional status and headers (ResponseInit) * **contentType** - optional, defaults to `'application/octet-stream'` --- --- url: 'https://docs-deserve.neabyte.com/rendering/streaming.md' description: >- Streaming template rendering in Deserve for faster time-to-first-byte responses. --- # Streaming Template Rendering Streaming template rendering sends HTML as it is produced, which lowers time-to-first-byte (TTFB) and keeps large pages feeling responsive. It is the progressive counterpart to the regular render covered in [Rendering Overview](/rendering/). ## Basic Concept Instead of waiting for the whole template to finish, streaming sends the HTML chunk by chunk: ```typescript // Regular render (blocking) - wait for everything to complete return await ctx.render('large-template', data) // Streaming render (progressive) - send chunk by chunk return await ctx.streamRender('large-template', data) ``` ![Side by side, ctx.render builds the whole HTML into one string and sends it all at once so the client waits, while ctx.streamRender compiles up front, returns a ReadableStream, and writes each node as produced so the first bytes leave early](/diagrams/stream-render-vs-blocking.png) ## Basic Usage ### 1. In Context Handler `ctx.streamRender()` returns a streaming HTML response, so awaiting it is all that a route needs: ![The route awaits ctx.streamRender, the engine resolves and compiles the template, creates a TransformStream, returns the readable side at once so response headers go out, then renders into the writable side in the background where a failure surfaces as a view error event](/diagrams/stream-render-pipeline.png) ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare function getUser(): DataRecord declare function getAnalytics(): DataRecord // ---cut--- // routes/dashboard.ts // Streaming render complex dashboard export async function GET(ctx: Context): Promise { return await ctx.streamRender('dashboard', { user: getUser(), analytics: getAnalytics() }) } ``` ### 2. Custom Response Headers The view engine lives in framework state, so `ctx.getState` reaches it for full control over the streamed response: ```typescript twoslash import type { Context, DataRecord, ViewEngine } from '@neabyte/deserve' declare const reportData: DataRecord // ---cut--- // Access view engine from framework state export async function GET(ctx: Context): Promise { const view = ctx.getState('view' as never) const stream = await view!.streamRender('report', reportData) return ctx.send.stream( stream, { headers: { 'Cache-Control': 'no-cache' } }, 'text/html; charset=utf-8' ) } ``` ## Template Support All DVE features from [Template Syntax](/rendering/syntax) work with streaming: ![Streaming loops the top-level template nodes and writes each produced chunk in order, so a text node flushes on its own, but an each block builds all its rows into one string first and then flushes as a single chunk, meaning the streaming granularity is per top-level node rather than per loop item](/diagrams/stream-render-chunks.png) ```html {{title}}
{{header}}
{{#each items as item}}

{{item.name}}

{{item.description}}

{{/each}} {{#if showFooter}}
{{footer}}
{{/if}} ``` ## Best Use Cases ### 1. Large Templates ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare function getTransactions(): Promise declare function calculateSummary(): DataRecord // ---cut--- // Report with thousands of data rows export async function GET(ctx: Context): Promise { return await ctx.streamRender('financial-report', { transactions: await getTransactions(), // 10,000+ items summary: calculateSummary() }) } ``` ### 2. Real-time Data ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare function getLatestMetrics(): DataRecord declare function getActiveAlerts(): DataRecord // ---cut--- // Dashboard with live data export async function GET(ctx: Context): Promise { return await ctx.streamRender('live-dashboard', { metrics: getLatestMetrics(), alerts: getActiveAlerts() }) } ``` ### 3. Progressive Enhancement ```typescript twoslash import type { Context, DataRecord } from '@neabyte/deserve' declare function getLayoutData(): DataRecord declare function getContent(): Promise declare function getAnalytics(): Promise // ---cut--- // Send skeleton first, data streams in export async function GET(ctx: Context): Promise { return await ctx.streamRender('progressive-app', { layout: getLayoutData(), // Fast content: await getContent(), // Slow analytics: await getAnalytics() // Very slow }) } ``` ## Migration from Regular Render ```typescript // Before (blocking) - wait for everything to complete export async function GET(ctx: Context): Promise { return await ctx.render('large-template', data) } // After (streaming) - send progressively export async function GET(ctx: Context): Promise { return await ctx.streamRender('large-template', data) } ``` Streaming rendering lifts performance for large templates and real-time pages, and the API stays the same single await that a regular render uses. ![A time to first byte comparison where render makes the client wait while the whole page is built so the first byte lands late, against streamRender which flushes the first node right after compile so the first byte lands early while later chunks keep arriving until the stream closes](/diagrams/stream-render-ttfb.png) --- --- url: 'https://docs-deserve.neabyte.com/rendering/syntax.md' description: 'DVE template syntax reference: variables, conditionals, loops, and includes.' --- # Template Syntax DVE templates are plain HTML with a small set of {{ }} tags for data, conditions, loops, and includes. Setup and the first render live in [Rendering Overview](/rendering/). ## Variables A {{ }} tag prints a value, and member access reaches nested data: ```html

{{username}}

{{user.name}}

{{user.profile.email}}

``` Lookups read only an object's own properties, so `__proto__`, `constructor`, and other inherited keys resolve to nothing. That blocks prototype pollution through user data. ## Conditionals {{#if}} renders a block when the value is truthy: ```html {{#if user.isAdmin}} {{/if}} ``` A condition pairs with {{else}} for the fallback branch, and blocks nest freely: ```html {{#if posts.length > 0}}
{{#each posts as post}}
{{post.title}}
{{/each}}
{{else}}

No posts found.

{{/if}} ``` ## Loops {{#each}} walks an array under an alias, with metadata variables for each pass: ```html {{#each users as u}}

{{u.name}}

{{u.email}}

Index: {{@index}}, First: {{@first}}, Last: {{@last}}, Total: {{@length}}
{{/each}} ``` **Each metadata:** * `@index` - Item index (0-based) * `@first` - Boolean true if first item * `@last` - Boolean true if last item * `@length` - Total number of items Each loop is capped by `maxIterations`, covered in [Performance and Limits](/rendering/performance#iteration-limit). ## Includes The `>` operator pulls another template into the current one: ```html {{> header.dve}}

Page Content

{{> footer.dve}} ``` Includes nest up to a fixed depth, covered in [Performance and Limits](/rendering/performance#include-depth-limit). ## Expressions DVE supports JavaScript-like expressions for lookups and operators: ```html

Hello {{ user?.name ?? 'Guest' }}.

Total: {{ 1 + 2 * 3 }}

{{#if age >= 18}}Adult{{/if}} ``` The grammar is a safe subset, not full JavaScript. These pieces are supported: * **Member access** - `user.name`, `user.profile.email`, and optional chaining `user?.name` * **Math** - `+`, `-`, `*`, `/`, `%`, and unary `+`, `-`, `!` * **Comparison** - `===`, `!==`, `==`, `!=`, `>`, `<`, `>=`, `<=` * **Logic** - `&&`, `||`, `??`, and the ternary `cond ? a : b` * **Literals** - numbers, single or double quoted strings, `true`, `false`, `null`, `undefined` * **Grouping** - parentheses, for example `(a + b) * c` To keep templates safe and predictable, the engine rejects anything outside that subset and throws a parse error. Function calls like `format(price)`, bracket indexing like `items[0]`, and assignment are not allowed. ## Raw Output Values are HTML escaped by default, and triple braces opt out for trusted markup only: ```html

{{userInput}}

{{{trustedHtml}}}

``` ## Layout Composition A layout is built by including smaller templates and dropping data into plain variables, so a shared shell wraps each page without any special slot mechanism: ```html {{title}}
{{> header.dve}}
{{{ content }}}
{{> footer.dve}}
``` The `content` value comes from the route data, and triple braces render it as raw HTML when the markup is already trusted. --- --- url: 'https://docs-deserve.neabyte.com/response/text.md' description: Send plain text responses with ctx.send.text(). --- # Text Responses The `ctx.send.text()` method creates plain text responses. ## Basic Usage ```typescript twoslash import type { Context } from '@neabyte/deserve' export function GET(ctx: Context): Response { // Sends text/plain by default return ctx.send.text('Hello World') } ``` ## With Status Codes ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function POST(ctx: Context): Response { // Reply Not Implemented with 501 return ctx.send.text('Not Implemented', { status: 501 }) } ``` ## Error Messages ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Plain text error with status 500 return ctx.send.text('Internal Server Error', { status: 500 }) } ``` ## Custom Headers ```typescript twoslash import type { Context } from '@neabyte/deserve' // ---cut--- export function GET(ctx: Context): Response { // Add headers through the options return ctx.send.text('Hello World', { headers: { 'Content-Language': 'en', 'X-Custom': 'value' } }) } ``` --- --- url: 'https://docs-deserve.neabyte.com/middleware/websocket.md' description: Upgrade requests to WebSocket connections with lifecycle callbacks in Deserve. --- # WebSocket Middleware > **Reference**: [Deno upgradeWebSocket API Documentation](https://docs.deno.com/api/deno/~/Deno.upgradeWebSocket) WebSocket middleware handles WebSocket connection upgrades, allowing real-time bidirectional communication between client and server. ## Basic Usage Apply WebSocket middleware using Deserve's built-in middleware: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // Upgrade /ws and greet on connect router.use( Mware.websocket({ listener: '/ws', onConnect: (socket, event, ctx) => { console.log('WebSocket connected:', ctx.url) socket.send('Welcome') } }) ) await router.serve(8000) ``` ## WebSocket Options ### `listener` Specify the path prefix for WebSocket upgrades: ```typescript listener: '/ws' // Matches /ws, /ws/chat, /ws/room/123, etc. listener: '/api/ws' // Matches /api/ws, /api/ws/data, etc. ``` **Important:** The middleware only upgrades requests that: * Carry the `Upgrade: websocket` header * Use the `GET` method * Have a path that starts with the `listener` value Without a `listener`, the middleware passes every request through and never upgrades. ### `allowedOrigins` Control which handshake origins are accepted, which guards against cross-site WebSocket hijacking: ```typescript allowedOrigins: '*' // Accept any origin allowedOrigins: ['https://example.com', 'https://app.example.com'] // Allowlist ``` When `allowedOrigins` is left undefined, only same-origin handshakes are accepted. A rejected origin returns **403 Forbidden**. ### `onConnect` Handle new WebSocket connections: ```typescript onConnect: (socket: WebSocket, event: Event, ctx: Context) => { console.log('Client connected:', ctx.url) socket.send(JSON.stringify({ type: 'welcome', message: 'Connected' })) } ``` ### `onMessage` Handle incoming WebSocket messages: ```typescript onMessage: (socket: WebSocket, event: MessageEvent, ctx: Context) => { console.log('Received:', event.data) try { const data = JSON.parse(event.data as string) socket.send(JSON.stringify({ echo: data })) } catch { socket.send(JSON.stringify({ error: 'Invalid JSON' })) } } ``` ### `onDisconnect` Handle WebSocket disconnections. The `CloseEvent` provides `code`, `reason`, and `wasClean`: ```typescript onDisconnect: (socket: WebSocket, event: CloseEvent, ctx: Context) => { console.log('Client disconnected:', event.code, event.reason, event.wasClean) } ``` ### `onError` Handle WebSocket errors: ```typescript onError: (socket: WebSocket, event: Event, ctx: Context) => { console.error('WebSocket error:', event, 'on', ctx.url) } ``` ## Complete Example ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router({ routesDir: './routes' }) router.use( Mware.websocket({ listener: '/ws', onConnect: (socket, event, ctx) => { console.log(`WebSocket connected: ${ctx.url}`) socket.send( JSON.stringify({ type: 'welcome', message: 'Connected to Deserve WebSocket server' }) ) }, onMessage: (socket, event, ctx) => { console.log(`Message from ${ctx.url}:`, event.data) try { const data = JSON.parse(event.data as string) if (data.type === 'ping') { socket.send( JSON.stringify({ type: 'pong', timestamp: Date.now() }) ) } else { socket.send( JSON.stringify({ type: 'echo', original: data, timestamp: Date.now() }) ) } } catch { socket.send( JSON.stringify({ type: 'error', message: 'Invalid JSON' }) ) } }, onDisconnect: (socket, event, ctx) => { console.log(`WebSocket disconnected: ${ctx.url} code=${event.code} reason=${event.reason}`) }, onError: (socket, event, ctx) => { console.error(`WebSocket error on ${ctx.url}:`, event) } }) ) await router.serve(8000) ``` ## Client-Side Usage Connect from a browser with the native WebSocket API: ```typescript twoslash const socket = new WebSocket('ws://localhost:8000/ws') socket.addEventListener('open', () => { console.log('Connected') socket.send( JSON.stringify({ type: 'message', text: 'Hello' }) ) }) socket.addEventListener('message', (event) => { const data = JSON.parse(event.data) console.log('Received:', data) }) socket.addEventListener('close', () => { console.log('Disconnected') }) socket.addEventListener('error', (error) => { console.error('Error:', error) }) ``` ## WebSocket Properties Access WebSocket properties inside handlers: ```typescript onConnect: (socket, event, ctx) => { console.log('URL:', socket.url) console.log('Protocol:', socket.protocol) console.log('State:', socket.readyState) // 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED } ``` ## Error Handling A rejected handshake routes through the error handler instead of throwing at setup: * **Disallowed origin** returns **403** with message `WebSocket handshake rejected because the Origin is not allowed`. * **Malformed upgrade** returns **400** with message `WebSocket handshake is malformed because ...`. To shape these responses, register a single handler with [`router.catch()`](/error-handling/object-details), or rely on the [default behavior](/error-handling/default-behavior). ## Integration with CORS WebSocket pairs with CORS-enabled clients like this: ```typescript twoslash import { Mware, Router } from '@neabyte/deserve' const router = new Router() // CORS handles HTTP, WebSocket handles upgrades router.use(Mware.cors({ origin: '*' })) router.use(Mware.websocket({ listener: '/ws' })) await router.serve(8000) ``` CORS middleware handles HTTP requests, while WebSocket middleware handles the upgrades, and the two run together without conflict. --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/worker-pool.md' description: >- Offloading CPU-bound work to a pool of Deno workers via the Deserve worker pool API. --- # Worker Pool > **Reference**: [Deno Workers API](https://docs.deno.com/runtime/manual/workers/) The worker pool offloads CPU-bound work to a pool of Deno Workers so the main thread stays responsive. Once a worker pool is configured, route handlers reach the worker handle through `ctx.getState('worker' as never)` and dispatch tasks with `run(payload)`. ## When to Use Use the worker pool when a route does **CPU-bound work** (e.g. heavy math, parsing, compression) that would block the event loop. For I/O-bound work (file, network), the main thread is usually enough. ## Basic Usage ### 1. Configure Router with Worker Pass `worker` when creating the router, along with a **script URL** that resolves to a module (for example via `import.meta.resolve()` or `URL.createObjectURL()` for inline code): ```typescript twoslash import { Router } from '@neabyte/deserve' // Resolve worker script as a module const workerScriptUrl = import.meta.resolve('./worker.ts') // Enable the pool on the router const router = new Router({ routesDir: './routes', worker: { scriptURL: workerScriptUrl, poolSize: 4 } }) await router.serve(8000) ``` ### 2. Implement the Worker Script The worker script must listen for `message` and reply with `postMessage`. Payload and result must be **structured-clone serializable** (no functions or symbols): ```typescript // worker.ts self.onmessage = (e: MessageEvent) => { const data = e.data as { iterations?: number } const n = Math.max(0, Number(data?.iterations) || 50_000) let value = 0 for (let i = 0; i < n; i++) { value += Math.sqrt(i) } self.postMessage({ done: true, value }) } ``` To report an error from the worker, send an object with `error: true` and optional `message`: ```typescript self.postMessage({ error: true, message: 'Computation failed' }) ``` ### 3. Use in a Route The worker handle lives in framework state, so `ctx.getState` reaches it with the `WorkerRunHandle` type. A router created without `worker` leaves the handle undefined, which is the moment to return 503: ```typescript twoslash // routes/heavy.ts import type { Context, WorkerRunHandle } from '@neabyte/deserve' export async function GET(ctx: Context): Promise { const worker = ctx.getState('worker' as never) if (!worker) { return ctx.send.json({ error: 'Worker not enabled' }, { status: 503 }) } const result = await worker.run<{ done: boolean; value: number }>({ iterations: 50_000 }) return ctx.send.json({ value: result?.value }) } ``` ## Router Options ### `scriptURL` Worker script URL. Must point to a **module** (Deno runs workers with `type: 'module'`). Typical sources: * **File path:** `import.meta.resolve('./worker.ts')` * **Inline script:** `URL.createObjectURL(new Blob([code], { type: 'application/javascript' }))` ### `poolSize` Number of workers in the pool. Default is **4**. Minimum is 1. Tasks are dispatched round-robin. ```typescript worker: { scriptURL: workerScriptUrl, poolSize: 8 } ``` ### `taskTimeoutMs` Per-task timeout in milliseconds. Default is **30000**. A task that runs longer rejects with a timeout error and the worker is respawned. ```typescript worker: { scriptURL: workerScriptUrl, taskTimeoutMs: 10_000 } ``` ## Complete Example (Inline Worker) Using an inline worker script with `Blob` and `createObjectURL`: ```typescript twoslash import { Router } from '@neabyte/deserve' const workerCode = ` self.onmessage = (e) => { const data = e.data || {} const n = Math.max(0, Number(data.iterations) || 50000) let value = 0 for (let i = 0; i < n; i++) value += Math.sqrt(i) self.postMessage({ done: true, value }) } export {} ` const workerScriptUrl = URL.createObjectURL( new Blob([workerCode], { type: 'application/javascript' }) ) const router = new Router({ routesDir: './routes', worker: { scriptURL: workerScriptUrl, poolSize: 4 } }) await router.serve(8000) ``` ## Error Handling * **No pool:** A router created without `worker` leaves `ctx.getState('worker' as never)` undefined. Return 503 or a clear message when the route requires a worker. * **Worker error:** When the worker calls `postMessage({ error: true, message: '...' })`, `worker.run()` rejects with an `Error` carrying that message. Without a message, the error reads `Worker returned an error with no message`. * **Worker crash:** When the worker throws or crashes, `run()` rejects with `Worker task failed before responding`. * **Task timeout:** When a task runs past `taskTimeoutMs` (default 30000), `run()` rejects with `Worker task exceeded ms timeout`. Catch a rejected task and forward it to the [centralized error handler](/error-handling/object-details): ```typescript try { const result = await worker.run(payload) return ctx.send.json(result) } catch (err) { // Route the failure through error handling return await ctx.handleError(500, err as Error) } ``` ## Structured Clone Only Payload and result are sent via `postMessage` / `onmessage`, so only **structured-clone serializable** data is allowed, which covers plain objects, arrays, primitives, `Date`, `RegExp`, `Map`, `Set`, and similar values. Functions, symbols, and non-cloneable class instances cannot cross that boundary. --- --- url: 'https://docs-deserve.neabyte.com/core-concepts/zero-dependency.md' description: >- Why Deserve ships with zero third-party dependencies and relies only on the Deno standard runtime. --- # Zero Dependency Deserve runs on the Deno runtime and nothing from npm. There is no `node_modules/` folder to install, audit, or worry about. ## Why It Matters Node's biggest scar is the supply chain. A fresh project pulls in hundreds of transitive packages, and any one of them can ship a compromised update overnight. That risk still haunts `node_modules/` every single day, and most teams never read the code they install. Deserve takes the other path. It builds on what Deno already provides and keeps the dependency tree out of the picture, so there is no npm registry in the loop and far less surface for a supply chain attack to land on. Less to trust means less that can break. ![A Node project pulls app code through the npm registry into hundreds of transitive dependencies where any update can be hostile, while a Deserve project uses only the Deno runtime and a few audited JSR modules](/diagrams/zero-dep-supply-chain.png) ## Following Deno's Vision Deno was designed around safer defaults, and this choice follows that lead. The runtime brings rich request handling, file watching, and security primitives out of the box, so reaching for an npm package is rarely the answer. The industry is moving toward caring more about the people who use the software, and shipping fewer moving parts is part of that care. ![Deserve draws request handling, file watching, security primitives, and permission flags straight from the built-in Deno runtime, so no npm package is needed](/diagrams/zero-dep-runtime-primitives.png) ## Secure by Default Security should be the starting point, not a later upgrade. That belief is not a promise of perfection, it is a direction. A smaller dependency tree, [process protection](/getting-started/server-configuration#process-protection), and [layered error handling](/error-handling/defense-in-depth) all point the same way, toward a server that stays safe even when something goes wrong. ![A process sentinel interposes known termination calls, so self-targeted Deno.exit and process.exit are blocked and unhandled rejections are trapped while a kill aimed at another pid still passes through, keeping the process alive and emitting a process error event](/diagrams/zero-dep-process-guard.png) ## Open and Auditable Every module that Deserve does rely on is open source and published on [JSR](https://jsr.io/), so the code is there to read, audit, and contribute to. Transparency is the point. Nothing hides behind a minified bundle, and anyone can check exactly what runs. ![What the guard protects against, self termination, uncaught faults, and denial of service, set beside what it does not do, since it is not a sandbox and untrusted code still runs, so it pairs with Deno permission flags and dependency review](/diagrams/zero-dep-best-effort.png) This pairs with the rest of the [philosophy](/core-concepts/philosophy): keep it simple, build on the platform, and stay honest about the trade-offs.