Application APIs

The optional APIs added in OPTZ 0.4 cover application state and request handling. Import only what your app uses; public static serving does not initialize these modules.

On this page

Runtime boundaries

ImportMain exportsRuntime
optz/sessionscreateSessions, memorySessionStore, SessionStoreWeb Crypto; provide your own persistent store
optz/sqliteopenDatabase, migrate, sqliteSessionStoreBun
optz/formsparseInput, validateInput, ValidationError, formValuesWeb-standard Request / FormData
optz/securitycsrf, assertSameOrigin, cors, secureHeaders, createRateLimiterWeb-standard middleware
optz/storagelocalStorageNode/Bun private local files
optz/streameventStreamWeb-standard streaming responses
optz/observeobserveOpt-in middleware

Keep private modules in .server.ts / .server.tsx or app/lib/server/. See Build a full-stack app for an executable integration of these APIs.

Sessions

createSessions<T>({ store, secure?, name?, maxAge?, now? }) returns read(request), create(data, previousRequest?) and destroy(request). read returns a session or undefined. create and destroy return a Set-Cookie header value; attach it to the outgoing response. The default lifetime is seven days (maxAge is in seconds).

// app/lib/sessions.server.ts — Bun deployment
import { createSessions } from "optz/sessions";
import { openDatabase, sqliteSessionStore } from "optz/sqlite";

const db = openDatabase(process.env.DATABASE_PATH!);
export const sessions = createSessions<{ userId: string }>({
  store: sqliteSessionStore(db),
  secure: true,
  maxAge: 60 * 60 * 24 * 7,
});

After verifying credentials, call sessions.create({ userId }, request) to mint a fresh token and revoke the previous cookie's identity. On logout, return await sessions.destroy(request) in Set-Cookie. The manager defaults to __Host-optz_session on HTTPS, with Secure, HttpOnly, SameSite=Lax and path /. Explicit secure: false is for local HTTP development.

A store implements get(key), set(key, { data, expiresAt }), and delete(key), synchronously or asynchronously. Keys are SHA-256 hashes of random 256-bit tokens; expiresAt is Unix time in milliseconds. memorySessionStore(maxEntries?) is bounded and process-local, for prototypes/tests. Use persistent shared storage for a distributed application.

SQLite and migrations

openDatabase(path) enables WAL, foreign keys and a 5-second busy timeout. Initialize state lazily if public builds should not create a database. migrate applies numbered SQL changes inside an immediate transaction and rejects changed checksums or out-of-order insertions.

import { openDatabase, migrate } from "optz/sqlite";

const db = openDatabase(process.env.DATABASE_PATH!);
migrate(db, [{
  id: 1,
  sql: `CREATE TABLE notes (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    title TEXT NOT NULL
  )`,
}]);

// Authenticate first; constrain each data operation by the authenticated owner.
export function notesFor(userId: string) {
  return db.query("SELECT id, title FROM notes WHERE user_id = ?").all(userId);
}

Add new migration IDs rather than modifying applied SQL. This adapter targets one Bun machine with persistent disk; other databases remain an application integration.

Input validation

parseInput(request, validator, { maxBytes?, timeoutMs? }) parses JSON, URL-encoded forms and multipart forms. Its validator can implement Standard Schema V1 (for example Zod or Valibot), or be a synchronous/asynchronous function. TypeScript annotations do not validate requests.

// app/api/preview.ts — validates input; does not persist anything
import type { ApiContext } from "optz";
import { parseInput, ValidationError } from "optz/forms";

export async function POST({ request }: ApiContext) {
  const values = await parseInput(request, input => {
    const title = (input as Record<string, unknown>)?.title;
    if (typeof title !== "string" || !title.trim() || title.length > 200) {
      throw new ValidationError({ title: ["Enter a title of 1–200 characters"] });
    }
    return { title: title.trim() };
  }, { maxBytes: 8192 });
  return Response.json(values, { headers: { "Cache-Control": "no-store" } });
}

Validation errors become safe 422 JSON responses with a fields map. Malformed input, oversized input and unsupported content types produce 400, 413 and 415 respectively. Input values are not echoed automatically. Repeated form fields become arrays. Use validateInput(validator, value) when parsing is already handled, or formValues(formData) to preserve repeated fields. Add authentication, ownership checks and CSRF before persisting mutations.

Request security

csrf(origin) checks unsafe request methods against your configured public origin. secureHeaders(overrides?) adds nosniff, a referrer policy and frame denial; provide your own Content-Security-Policy appropriate to the application. cors({ origins, credentials?, methods?, headers? }) uses explicit origins and handles allowed preflights; wildcard origins are rejected.

// app/middleware.ts — configure PUBLIC_URL before starting the app
import { csrf, secureHeaders } from "optz/security";

export default [
  csrf(process.env.PUBLIC_URL!),
  secureHeaders(),
];

createRateLimiter({ limit, windowMs, maxKeys?, now? }).consume(key) returns { allowed, remaining, retryAfter }, with retry seconds. It uses bounded fixed windows and refuses new keys when full. Choose short trusted identity keys; unverified forwarded headers are not identities. The limiter is local to one process.

Private attachments

localStorage(directory) from optz/storage returns save(file, options?), response(key, downloadName?) and remove(key). This is private server file storage, unrelated to the browser's window.localStorage.

// app/lib/files.server.ts
import { localStorage } from "optz/storage";

export const files = localStorage(process.env.UPLOAD_DIR!);
export async function saveTextAttachment(file: File) {
  return files.save(file, { maxBytes: 1 << 20, types: ["text/plain"] });
  // Returns { key, size, name, type }; persist ownership with this metadata.
}

Writes use random keys, private permissions and streaming size limits. Reads reject symlinks and return attachment-only responses with nosniff and private/no-store. Authorize the owner before calling response, enforce quotas and bound concurrent uploads in your app. A MIME allowlist checks declared transport types; it is not file-content inspection. Keep the directory outside public/. See the starter upload route.

Server-sent events

eventStream(request, source, { maxEventBytes?, onClose? }) converts an async iterable of { data, event?, id?, retry? } into SSE. The default maximum encoded event is 64 KiB. Events are pulled with backpressure; metadata is checked for invalid framing.

// app/api/ticks.ts — public example, Bun/Node timer implementation
import type { ApiContext } from "optz";
import { eventStream } from "optz/stream";
import { setTimeout } from "node:timers/promises";

export function GET({ request }: ApiContext) {
  return eventStream(request, async function* (signal) {
    while (!signal.aborted) {
      yield { event: "tick", data: { time: Date.now() } };
      try { await setTimeout(10000, undefined, { signal }); }
      catch { return; }
    }
  }, { maxEventBytes: 4096 });
}

Observe the supplied signal during pending work. Use onClose to release connection budgets. For private events, authenticate before opening the stream and recheck expiry/revocation while it runs, as the starter does. SSE does not provide durable pub/sub, bidirectional WebSockets or a task queue.

Request observation

observe({ onRequest?, onError? }) adds X-Request-Id and Server-Timing. Events contain an ID, method, pathname and duration; successful response events also include status. Query strings, credentials and bodies are excluded. Callback failures are caught and logged.

import { observe } from "optz/observe";

export default [observe({
  onRequest: ({ id, method, pathname, status, durationMs }) => {
    console.log(JSON.stringify({ id, method, pathname, status, durationMs }));
  },
})];

Callbacks do not delay response completion. This is an integration hook, not a bundled telemetry backend or a delivery guarantee. Combine it with your other middleware in one array.