Server

Agent-native apps use Nitro for the server layer. Nitro is included automatically via the defineConfig() Vite plugin — you get file-based API routing, server plugins, and deploy-anywhere presets out of the box.

File-Based Routing

API routes live in server/routes/. Nitro auto-discovers them based on file name and path:

server/routes/
  api/
    hello.get.ts          → GET  /api/hello
    items/
      index.get.ts        → GET  /api/items
      index.post.ts       → POST /api/items
      [id].get.ts         → GET  /api/items/:id
      [id].delete.ts      → DELETE /api/items/:id
      [id]/
        archive.patch.ts  → PATCH /api/items/:id/archive

Each route file exports a default defineEventHandler:

// server/routes/api/items/index.get.ts
import { defineEventHandler } from "h3";
import fs from "fs/promises";

export default defineEventHandler(async () => {
  const files = await fs.readdir("./data/items");
  const items = await Promise.all(
    files
      .filter((f) => f.endsWith(".json"))
      .map(async (f) => JSON.parse(await fs.readFile(`./data/items/${f}`, "utf-8"))),
  );
  return items;
});

Route naming conventions

File name patternHTTP methodExample path
index.get.tsGET/api/items
index.post.tsPOST/api/items
[id].get.tsGET/api/items/:id
[id].patch.tsPATCH/api/items/:id
[id].delete.tsDELETE/api/items/:id
[...slug].get.tsGET/api/items/* (catch-all)

Accessing route parameters

import { defineEventHandler, getRouterParam, readBody, getQuery } from "h3";

// GET /api/items/:id
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  const { filter } = getQuery(event);
  // ...
});

Server Plugins

Cross-cutting concerns — file watchers, file sync, scheduled jobs, auth — go in server/plugins/. Nitro runs these at startup before serving requests:

// server/plugins/file-sync.ts
import { defineNitroPlugin } from "@agent-native/core";
import { createFileSync } from "@agent-native/core/adapters/sync";

export default defineNitroPlugin(async () => {
  const result = await createFileSync({ contentRoot: "./data" });
  if (result.status === "error") {
    console.warn(`[app] File sync failed: ${result.reason}`);
  }
});

Shared State Between Plugins and Routes

Use a shared module in server/lib/ to pass state from plugins to route handlers:

// server/lib/watcher.ts
import { createFileWatcher } from "@agent-native/core";
import type { SSEHandlerOptions } from "@agent-native/core";

export const watcher = createFileWatcher("./data");
export const sseExtraEmitters: NonNullable<SSEHandlerOptions["extraEmitters"]> = [];

export let syncResult: any = { status: "disabled" };
export function setSyncResult(result: any) {
  syncResult = result;
  if (result.status === "ready" && result.sseEmitter) {
    sseExtraEmitters.push(result.sseEmitter);
  }
}

The plugin populates the state at startup; route handlers read it at request time.

createFileWatcher(dir, options?)

Creates a chokidar file watcher for real-time file change detection:

import { createFileWatcher } from "@agent-native/core";

const watcher = createFileWatcher("./data");
// watcher emits "all" events: (eventName, filePath)

Options

OptionTypeDescription
ignoredanyGlob patterns or regex to ignore
emitInitialbooleanEmit events for initial file scan. Default: false

createSSEHandler(watcher, options?)

Creates an H3 event handler that streams file changes as Server-Sent Events:

// server/routes/api/events.get.ts
import { createSSEHandler } from "@agent-native/core";
import { watcher, sseExtraEmitters } from "../../lib/watcher.js";

export default createSSEHandler(watcher, {
  extraEmitters: sseExtraEmitters,
  contentRoot: "./data",
});

Each SSE message is JSON: { "type": "change", "path": "data/file.json" }

Options

OptionTypeDescription
extraEmittersArray<{ emitter, event }>Additional EventEmitters to stream
contentRootstringRoot directory used to relativize paths in events

createServer(options?)

Optional helper that creates a pre-configured H3 app with CORS middleware and a health-check route. Returns { app, router }. Useful for programmatic route registration when file-based routing doesn't fit:

import { createServer } from "@agent-native/core";
import { defineEventHandler } from "h3";

const { app, router } = createServer();
router.get("/api/items", defineEventHandler(listItems));

mountAuthMiddleware(app, accessToken)

Mounts session-cookie authentication onto an H3 app. Serves a login page for unauthenticated browser requests and returns 401 for unauthenticated API requests.

import { mountAuthMiddleware } from "@agent-native/core";

mountAuthMiddleware(app, process.env.ACCESS_TOKEN!);

Adds two routes automatically: POST /api/auth/login and POST /api/auth/logout.

createProductionAgentHandler(options)

Creates an H3 SSE handler at POST /api/agent-chat that runs an agentic tool loop using Claude. Each script's run() function is registered as a tool the agent can invoke.

import { createProductionAgentHandler } from "@agent-native/core";
import { scripts } from "./scripts/registry.js";
import { readFileSync } from "fs";

const agent = createProductionAgentHandler({
  scripts,
  systemPrompt: readFileSync("agents/system-prompt.md", "utf-8"),
});

Options

OptionTypeDescription
scriptsRecord<string, ScriptEntry>Map of script name → { tool, run } entries
systemPromptstringSystem prompt for the embedded agent
apiKeystringAnthropic API key. Default: ANTHROPIC_API_KEY env
modelstringModel to use. Default: claude-sonnet-4-6