--- url: /nile/guide/start/getting-started.md --- # Getting Started with Nile Nile is a functional-first, type-safe backend framework built on Hono. It works with **Bun**, **Node.js**, and **Deno**, uses Zod for validation, and returns Results from all action handlers using the `Ok` / `Err` pattern from `slang-ts`. ## 1. Installation ### Scaffold with the CLI (recommended) The CLI creates a working project with services, database setup, and dev tooling: ```bash npx @nilejs/cli new my-app cd my-app && bun install && bun run dev ``` The generated project includes a tasks service, PGLite database, Drizzle ORM, and a running server. You can add more services and actions with `npx @nilejs/cli generate service ` and `npx @nilejs/cli generate action `. To extract Zod schemas and TypeScript types from your actions, run `npx @nilejs/cli generate schema`. ### Manual install :::tabs @tab Bun ```bash bun add @nilejs/nile zod slang-ts ``` @tab npm ```bash npm install @nilejs/nile zod slang-ts ``` @tab pnpm ```bash pnpm add @nilejs/nile zod slang-ts ``` ::: ## 2. Quick Start ### 2.1 Create Actions Actions are the core building blocks. Each action has a name, optional Zod validation schema, and a handler that returns a `Result`. ```typescript // services/todos/create.ts import { Ok } from "slang-ts"; import z from "zod"; import { createAction, type Action } from "@nilejs/nile"; const createTodoSchema = z.object({ title: z.string().min(1, "Title is required"), completed: z.boolean().default(false), }); const createTodoHandler = (data: Record) => { const todo = { id: crypto.randomUUID(), title: data.title as string, completed: (data.completed as boolean) ?? false, }; return Ok({ todo }); }; export const createTodoAction: Action = createAction({ name: "create", description: "Create a new todo", validation: createTodoSchema, handler: createTodoHandler, }); ``` ### 2.2 Group Actions into a Service ```typescript // services/todos/list.ts import { Ok } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; const listTodoHandler = () => { return Ok({ todos: [ { id: "1", title: "Learn Nile", completed: false }, { id: "2", title: "Build an API", completed: true }, ], }); }; export const listTodoAction: Action = createAction({ name: "list", description: "List all todos", handler: listTodoHandler, }); ``` ```typescript // services/todos.ts import { createServices, type Services } from "@nilejs/nile"; import { createTodoAction } from "./create"; import { listTodoAction } from "./list"; export const services: Services = createServices([ { name: "todos", description: "Todo list management", actions: [createTodoAction, listTodoAction], }, ]); ``` ### 2.3 Create and Start the Server ```typescript // server.ts import { createNileServer } from "@nilejs/nile"; import { services } from "./services/todos"; const server = createNileServer({ serverName: "my-app", services, rest: { baseUrl: "/api", port: 8000, }, }); if (server.rest) { const { fetch } = server.rest.app; Bun.serve({ fetch, port: 8000 }); console.log("Server running at http://localhost:8000"); } ``` Run with Bun: ```bash bun run server.ts ``` ### 2.4 Invoke Your Actions Nile uses a single POST endpoint with an intent-driven payload: ```bash # List todos curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "execute", "service": "todos", "action": "list", "payload": {} }' # Create a todo curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "execute", "service": "todos", "action": "create", "payload": { "title": "Ship Nile", "completed": false } }' ``` ## 3. Project Structure ``` my-api/ ├── server.ts # Entry point ├── services/ │ ├── todos.ts # Service definition │ └── todos/ │ ├── create.ts # Action: create todo │ └── list.ts # Action: list todos └── package.json ``` ## 4. Next Steps - Learn about [Actions](/guide/basics/actions) and [Services](/guide/basics/services) - Explore the [Context](/guide/basics/context) for accessing resources like databases - Set up a [database layer with model files](/guide/internals/db) for structured data access - See [Server Configuration](/guide/internals/server) for more options *This documentation reflects the current implementation and is subject to evolution.* --- url: /nile/guide/basics/actions.md --- # Actions Actions are the core building blocks of Nile. Each action represents a single operation that can be called via the REST-RPC interface. ## createAction ```typescript import { Ok, Err } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; export const myAction: Action = createAction({ name: "actionName", description: "What this action does", handler: (data) => { // Return Ok with data on success return Ok({ result: "success" }); // Or return Err on failure // return Err("Something went wrong"); }, }); ``` ## Options | Option | Type | Description | |--------|------|-------------| | `name` | `string` | Unique identifier for the action. Duplicates within the same service throw on boot. | | `description` | `string` | Human-readable description | | `validation` | `z.ZodTypeAny \| null` | Optional Zod schema for input validation | | `handler` | `ActionHandler` | The function that executes when the action is called | | `isProtected` | `boolean` | If true, requires authentication | | `visibility` | `{ rest?: boolean; rpc?: boolean }` | Control which interfaces expose this action | ## Handler Signature The handler receives input data and context, and must return a `Result` from `slang-ts`: ```typescript import type { Result } from "slang-ts"; import type { NileContext } from "@nilejs/nile"; type ActionHandler = ( data: Record, context?: NileContext ) => Result | Promise>; ``` Use `Ok(data)` for success and `Err(error)` for failures: ```typescript handler: (data) => { if (!data.requiredField) { return Err("Required field is missing"); } return Ok({ id: "1", name: "Item" }); }, ``` ## Example: Action with Validation ```typescript // services/tasks/create.ts import { Ok } from "slang-ts"; import z from "zod"; import { createAction, type Action } from "@nilejs/nile"; const createTaskSchema = z.object({ title: z.string().min(1, "Title is required"), status: z.enum(["pending", "in-progress", "done"]).default("pending"), }); const createTaskHandler = (data: Record) => { const task = { id: crypto.randomUUID(), title: data.title as string, status: (data.status as string) ?? "pending", }; return Ok({ task }); }; export const createTaskAction: Action = createAction({ name: "create", description: "Create a new task", validation: createTaskSchema, handler: createTaskHandler, }); ``` ## Multiple Actions Actions are typically defined in separate files and then grouped into a service: ```typescript // services/tasks/create.ts import { Ok } from "slang-ts"; import z from "zod"; import { createAction, type Action } from "@nilejs/nile"; const createTaskSchema = z.object({ title: z.string().min(1, "Title is required"), }); const createTaskHandler = (data: Record) => { return Ok({ task: { id: crypto.randomUUID(), title: data.title } }); }; export const createTaskAction: Action = createAction({ name: "create", description: "Create a new task", validation: createTaskSchema, handler: createTaskHandler, }); ``` ```typescript // services/tasks/list.ts import { Ok } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; export const listTaskAction: Action = createAction({ name: "list", description: "List all tasks", handler: () => Ok({ tasks: [] }), }); ``` Then group them in the service config: ```typescript // services/tasks.ts import { createServices, type Services } from "@nilejs/nile"; import { createTaskAction } from "./tasks/create"; import { listTaskAction } from "./tasks/list"; export const services: Services = createServices([ { name: "tasks", description: "Task management", actions: [createTaskAction, listTaskAction], }, ]); ``` ## Accessing Context The handler receives a second parameter with access to resources: ```typescript handler: (data, context) => { // Access database from context const users = await context?.resources?.database?.query.users.findMany(); // Access logger context?.resources?.logger?.info({ atFunction: "myAction", message: "Processing" }); return Ok({ count: users.length }); } ``` --- url: /nile/guide/basics/services.md --- # Services Services group related actions together. A service is a logical container that organizes your actions under a common namespace. ## Defining a Service Services are plain objects with a name, description, and array of actions. You can define them directly or use the `createServices` helper for better type inference: ```typescript import { createServices, type Services } from "@nilejs/nile"; import { createTaskAction } from "./tasks/create"; import { listTaskAction } from "./tasks/list"; export const services: Services = createServices([ { name: "tasks", description: "Task management with CRUD operations", actions: [createTaskAction, listTaskAction], }, ]); ``` > **Note:** The `createServices` helper is optional - you can also pass the array directly (`services: [...]`). Similarly, you can use `createActions` to wrap action arrays, or pass them directly - both approaches work. ## Options | Option | Type | Description | |--------|------|-------------| | `name` | `string` | Unique identifier for the service. Duplicate service names throw on boot. | | `description` | `string` | Human-readable description | | `actions` | `Action[]` | Array of actions belonging to this service | | `meta` | `Record` | Optional metadata for the service | ## Example: Full Service Setup ```typescript // services/tasks/create.ts import { Ok } from "slang-ts"; import z from "zod"; import { createAction, type Action } from "@nilejs/nile"; const createTaskSchema = z.object({ title: z.string().min(1, "Title is required"), status: z.enum(["pending", "in-progress", "done"]).default("pending"), }); const createTaskHandler = (data: Record) => { const task = { id: crypto.randomUUID(), title: data.title as string, status: (data.status as string) ?? "pending", }; return Ok({ task }); }; export const createTaskAction: Action = createAction({ name: "create", description: "Create a new task", validation: createTaskSchema, handler: createTaskHandler, }); ``` ```typescript // services/tasks/list.ts import { Ok } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; const listTaskHandler = () => { return Ok({ tasks: [ { id: "1", title: "Learn Nile", status: "pending" }, { id: "2", title: "Build something", status: "done" }, ], }); }; export const listTaskAction: Action = createAction({ name: "list", description: "List all tasks", handler: listTaskHandler, }); ``` ```typescript // services/tasks.ts import { createServices, type Services } from "@nilejs/nile"; import { createTaskAction } from "./tasks/create"; import { listTaskAction } from "./tasks/list"; export const services: Services = createServices([ { name: "tasks", description: "Task management operations", actions: [createTaskAction, listTaskAction], }, ]); ``` ## Using Services in the Server ```typescript // server.ts import { createNileServer } from "@nilejs/nile"; import { services } from "./services/tasks"; const server = createNileServer({ serverName: "my-app", services, rest: { baseUrl: "/api", port: 8000, }, }); if (server.rest) { const { fetch } = server.rest.app; Bun.serve({ fetch, port: 8000 }); console.log("Server running at http://localhost:8000"); } ``` ## Invoking Actions Once the server is running, invoke actions via POST to `/api/services`: ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "execute", "service": "tasks", "action": "list", "payload": {} }' ``` --- url: /nile/guide/basics/context.md --- # Context `NileContext` provides access to request context, session data, and shared resources throughout your application. ## Accessing Context The context is passed as the second parameter to your action handler: ```typescript import { Ok } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; export const myAction: Action = createAction({ name: "myAction", description: "Example action", handler: (data, context) => { // Use context here return Ok({ result: "success" }); }, }); ``` You can also use `getContext()` to access context from anywhere: ```typescript import { getContext } from "@nilejs/nile"; const context = getContext(); ``` ## What's in Context | Property | Type | Description | |----------|------|-------------| | `rest` | `HonoContext \| undefined` | Hono request context (when called via HTTP) | | `ws` | `WebSocketContext \| undefined` | WebSocket context (when called via WS) | | `rpc` | `RPCContext \| undefined` | RPC context (when called via RPC) | | `sessions` | `Sessions` | Session data per interface (`rest`, `ws`, `rpc`) | | `resources` | `Resources \| undefined` | Shared resources (logger, database, cache) | | `get` / `set` | `(key: string) => T` | General-purpose key-value store | | `getSession` / `setSession` | `(name: keyof Sessions, ...) => ...` | Session access per interface | ## Accessing Resources Resources are provided at server startup and available via context: ```typescript handler: (data, context) => { const logger = context?.resources?.logger; // Access logger logger?.info({ atFunction: "myAction", message: "Action executed", data: { timestamp: Date.now() }, }); // Access database (passed in server config) const db = context?.resources?.database; return Ok({ result: "done" }); }, ``` ## Session Management Store and retrieve session data per interface: ```typescript handler: (data, context) => { // Set session data context?.setSession("rest", { userId: "123", role: "admin" }); // Get session data const session = context?.getSession("rest"); console.log(session?.userId); // "123" return Ok({ session }); }, ``` ## Type-Safe Context Pass a generic to get type safety for your database: ```typescript import type { MyDatabase } from "./db"; handler: (data, context) => { const db = context?.resources?.database as MyDatabase | undefined; // db is typed as MyDatabase return Ok({}); } ``` ## Example: Full Context Usage ```typescript // services/users/create.ts import { Ok, Err } from "slang-ts"; import z from "zod"; import { createAction, type Action } from "@nilejs/nile"; const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), }); const createUserHandler = (data: Record, context) => { const { name, email } = data as { name: string; email: string }; // Log the action context?.resources?.logger?.info({ atFunction: "createUser", message: "Creating user", data: { email }, }); // Access database // const db = context?.resources?.database; // const user = await db.users.create({ name, email }); const user = { id: crypto.randomUUID(), name, email }; return Ok({ user }); }; export const createUserAction: Action = createAction({ name: "create", description: "Create a new user", validation: createUserSchema, handler: createUserHandler, }); ``` ## Why a Single Context? `NileContext` is a **server-level singleton by design**. Rather than creating isolated per-request contexts, Nile uses a single shared context as the **source of truth** for the entire runtime. This means every action, hook, and handler operates on the same page — they share the same resources, sessions, and configuration state. This is intentional for several reasons: - **Consistency** — All parts of the pipeline (auth, hooks, handlers) see the same context. There is no risk of stale or divergent state between middleware layers. - **Simplicity** — One context object is easy to reason about. No context factories, no request-scoped DI containers, no hidden lifecycles. - **Composition** — Hooks that reference other actions (via `hooks.before` / `hooks.after`) naturally share state through the same context, enabling clean pipeline composition. - **Resource sharing** — Database connections, loggers, and caches are attached once at boot and available everywhere without re-injection. Per-request data (like auth results) is written to the context at the start of each execution and reset between requests via `resetHookContext`. This gives you request-scoped data within the single-context model. ```typescript // Auth result is set per-request, then accessible throughout the pipeline handler: (data, context) => { const user = context?.getUser(); // Set during auth step const auth = context?.getAuth(); // Full auth result return Ok({ userId: user?.userId }); }, ``` ## Server Configuration with Resources Pass resources when creating the server: ```typescript // server.ts import { createNileServer, createLogger } from "@nilejs/nile"; import { services } from "./services"; const logger = createLogger("my-api", { chunking: "monthly" }); const server = createNileServer({ serverName: "my-app", services, resources: { logger, database: myDatabaseInstance, }, rest: { baseUrl: "/api", port: 8000, }, }); ``` --- url: /nile/guide/basics/auth.md --- # Authentication Nile includes built-in JWT authentication via `hono/jwt`. Protected actions require a valid JWT token before the handler executes. ## Configuration Pass an `auth` object to your server config: ```typescript import { createNileServer } from "@nilejs/nile"; const server = createNileServer({ name: "MyApp", services: [/* ... */], auth: { secret: process.env.JWT_SECRET!, method: "header", // "header" (default) or "cookie" }, rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], }, }); ``` ## Auth Config Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `secret` | `string` | *required* | JWT secret for token verification | | `method` | `"header" \| "cookie"` | `"header"` | Where to look for the token | | `headerName` | `string` | `"authorization"` | Header name (when method is `"header"`) | | `cookieName` | `string` | `"auth_token"` | Cookie name (when method is `"cookie"`) | ## Protecting Actions Set `isProtected: true` on any action that requires authentication: ```typescript import { Ok, Err } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; export const getProfile: Action = createAction({ name: "getProfile", description: "Get the current user's profile", isProtected: true, handler: (data, context) => { const user = context?.getUser(); if (!user) return Err("Not authenticated"); return Ok({ userId: user.userId, organizationId: user.organizationId, }); }, }); ``` When `isProtected` is `true` and `auth` is configured on the server: 1. The engine extracts the JWT from the request (header or cookie) 2. Verifies the token signature using `hono/jwt` 3. Extracts `userId` and `organizationId` from the claims 4. Populates `context.authResult` before the handler runs 5. If verification fails, the action returns an error without executing Actions without `isProtected` (or with `isProtected: false`) skip auth entirely. ## Accessing Auth Data Inside any handler or hook, use the context accessors: ```typescript // Full auth result (userId, organizationId, raw claims) const auth = context?.getAuth(); // { userId: "usr_123", organizationId: "org_456", claims: { ... } } // Convenience: user object with claims spread const user = context?.getUser(); // { userId: "usr_123", organizationId: "org_456", role: "admin", ... } ``` Both return `undefined` when no authentication occurred (e.g., unprotected actions). ## JWT Claims Mapping The JWT handler extracts identity fields from standard and common claim names: | Field | Supported claim names | |-------|----------------------| | `userId` | `userId`, `id`, `sub` | | `organizationId` | `organizationId`, `organization_id`, `orgId` | All other claims are preserved in the `claims` object and spread into `getUser()`. ## Token Sources ### Authorization Header (default) ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ``` ```typescript auth: { secret: "your-secret", method: "header", // headerName: "authorization" (default) } ``` ### Cookie ```typescript auth: { secret: "your-secret", method: "cookie", cookieName: "session_token", } ``` ## Custom Auth with Hooks For auth logic beyond JWT (RBAC, API keys, OAuth sessions), use `onBeforeActionHandler` as a middleware gate: ```typescript const server = createNileServer({ name: "MyApp", services: [/* ... */], auth: { secret: process.env.JWT_SECRET! }, onBeforeActionHandler: async (request, context) => { const user = context.getUser(); if (!user) return; // Let the engine's built-in auth handle it // Custom RBAC check const action = request.action; const requiredRole = action.accessControl?.[0]; if (requiredRole && user.role !== requiredRole) { return Err(`Requires role: ${requiredRole}`); } return Ok(request.payload); }, }); ``` The hook runs after JWT verification but before the action handler, giving you access to the verified user data for custom authorization logic. ## Example: Full Setup ```typescript import { createNileServer, createAction, type Action } from "@nilejs/nile"; import { Ok, Err } from "slang-ts"; import z from "zod"; // Public action — no auth required const listItems: Action = createAction({ name: "listItems", description: "List all items", handler: () => Ok({ items: [] }), }); // Protected action — requires valid JWT const createItem: Action = createAction({ name: "createItem", description: "Create a new item", isProtected: true, validation: z.object({ title: z.string().min(1) }), handler: (data, context) => { const user = context?.getUser(); return Ok({ id: crypto.randomUUID(), title: data.title, createdBy: user?.userId, }); }, }); const server = createNileServer({ name: "ItemService", services: [ { name: "items", description: "Item management", actions: [listItems, createItem], }, ], auth: { secret: process.env.JWT_SECRET!, method: "header", }, rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], }, }); export default server.rest?.app; ``` --- url: /nile/guide/basics/uploads.md --- # File Uploads Nile handles multipart form-data uploads through the same single POST endpoint. Files are parsed, validated through a 7-step chain, and delivered to your action handler as a structured payload. ## Configuration Enable uploads in your REST config: ```typescript const server = createNileServer({ name: "MyApp", services: [/* ... */], rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], uploads: { enforceContentType: true, limits: { maxFiles: 5, maxFileSize: 5 * 1024 * 1024, // 5MB per file minFileSize: 1, // reject zero-byte files maxTotalSize: 20 * 1024 * 1024, // 20MB total maxFilenameLength: 128, }, allow: { mimeTypes: ["image/png", "image/jpeg", "application/pdf"], extensions: [".png", ".jpg", ".jpeg", ".pdf"], }, }, }, }); ``` ### Upload Config Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `enforceContentType` | `boolean` | `false` | Enforce action-level content-type matching | | `limits.maxFiles` | `number` | `10` | Maximum number of files per request | | `limits.maxFileSize` | `number` | `10MB` | Maximum size per individual file (bytes) | | `limits.minFileSize` | `number` | `1` | Minimum file size (rejects zero-byte files) | | `limits.maxTotalSize` | `number` | `20MB` | Maximum combined size of all files | | `limits.maxFilenameLength` | `number` | `128` | Maximum filename character length | | `allow.mimeTypes` | `string[]` | `["image/png", "image/jpeg", "application/pdf"]` | Allowed MIME types | | `allow.extensions` | `string[]` | `[".png", ".jpg", ".jpeg", ".pdf"]` | Allowed file extensions | ## Sending Uploads Form-data requests must include the RPC routing fields (`intent`, `service`, `action`) as string fields alongside file fields. ### Using the Nile Client ```typescript import { createNileClient } from "@nilejs/client"; const nile = createNileClient({ baseUrl: "http://localhost:8000/api/v1" }); const { error, data } = await nile.upload({ service: "documents", action: "upload", files: { document: new File(["content"], "report.pdf", { type: "application/pdf" }), }, fields: { title: "Q4 Report", category: "finance", }, }); ``` ### Using fetch Directly ```typescript const formData = new FormData(); formData.append("intent", "execute"); formData.append("service", "documents"); formData.append("action", "upload"); formData.append("title", "Q4 Report"); formData.append("document", file); const response = await fetch("http://localhost:8000/api/v1/services", { method: "POST", body: formData, }); ``` ### Using curl ```bash curl -X POST http://localhost:8000/api/v1/services \ -F "intent=execute" \ -F "service=documents" \ -F "action=upload" \ -F "title=Q4 Report" \ -F "document=@./report.pdf" ``` ## Action Handler Your action handler receives a `StructuredPayload` with `fields` and `files` separated: ```typescript import { Ok, Err } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; const uploadDocument: Action = createAction({ name: "upload", description: "Upload a document", isSpecial: { contentType: "multipart/form-data", uploadMode: "flat", }, handler: (data, context) => { const { fields, files } = data as { fields: Record; files: Record; }; const title = fields.title as string; const document = files.document as File; // Process the file (save to storage, etc.) return Ok({ title, filename: document.name, size: document.size, type: document.type, }); }, }); ``` ### Action-Level Config The `isSpecial` field on an action controls upload behavior: | Option | Type | Description | |--------|------|-------------| | `contentType` | `"multipart/form-data" \| "application/json" \| "other"` | Expected content type for this action | | `uploadMode` | `"flat" \| "structured"` | Parsing mode (default: `"flat"`) | ## Parsing Modes ### Flat Mode (default) Rejects requests where the same form-data key carries both files and string fields. This prevents ambiguous payloads. ``` ✓ document=@file.pdf, title="Report" (different keys) ✗ data=@file.pdf, data="some string" (same key, mixed types) ``` ### Structured Mode Allows any combination of keys. Files and fields are separated into their respective buckets, with duplicate keys aggregated into arrays. ``` ✓ attachment=@file1.pdf, attachment=@file2.pdf (array of files) ✓ tag=frontend, tag=docs (array of strings) ``` ## Validation Chain Every upload request passes through a 7-step validation chain that fails fast on the first error: 1. **Filename length** — rejects files with names exceeding the configured limit 2. **Zero-byte detection** — rejects empty files 3. **Minimum size** — rejects files smaller than the threshold 4. **File count** — rejects requests exceeding the max file count 5. **Per-file size** — rejects individual files exceeding the size limit 6. **Total size** — rejects requests where combined file size exceeds the limit 7. **MIME + extension allowlist** — rejects files that don't match both the allowed MIME type and extension ## Error Responses Validation errors return structured error data with the `error_category` field: ```json { "status": false, "message": "upload limit exceeded", "data": { "error_category": "validation", "limit": "maxFileSize", "max": 5242880, "files": [{ "name": "huge-video.mp4", "size": 104857600 }] } } ``` ```json { "status": false, "message": "file type not allowed", "data": { "error_category": "validation", "rejected": [{ "name": "script.exe", "type": "application/x-msdownload" }], "allowed": { "mimeTypes": ["image/png", "image/jpeg", "application/pdf"], "extensions": [".png", ".jpg", ".jpeg", ".pdf"] } } } ``` ```json { "status": false, "message": "mixed key types not allowed", "data": { "error_category": "validation", "conflicts": ["data"], "hint": "Same key cannot be used for both files and fields" } } ``` ## Content-Type Enforcement When `enforceContentType` is enabled and an action specifies `isSpecial.contentType`, Nile checks that the incoming request's content type matches. Mismatches return `415 Unsupported Media Type`. This is useful when certain actions should only accept file uploads and reject JSON requests. --- url: /nile/guide/basics/file-serving.md --- # Static File Serving Nile can serve static files from a local directory through the REST interface. This is useful for serving images, documents, or any assets your application needs to expose over HTTP. ## Configuration Enable static file serving with `enableStatic` and specify your server runtime: ```typescript const server = createNileServer({ name: "MyApp", runtime: "bun", // or "node" services: [/* ... */], rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], enableStatic: true, }, }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `enableStatic` | `boolean` | `false` | Enable static file serving at `/assets/*` | | `runtime` | `"bun" \| "node"` | `"bun"` | Server runtime — determines which adapter is used | ## How It Works When `enableStatic` is `true`, Nile registers a middleware on `/assets/*` that serves files from a `./assets` directory relative to your project root. ``` your-project/ ├── assets/ │ ├── logo.png │ ├── styles.css │ └── docs/ │ └── readme.pdf ├── src/ │ └── index.ts └── package.json ``` These files are then accessible at: ``` GET /assets/logo.png GET /assets/styles.css GET /assets/docs/readme.pdf ``` ## Cross-Runtime Support Nile dynamically imports the correct `serveStatic` adapter based on the `runtime` value: | Runtime | Adapter | |---------|---------| | `bun` | `hono/bun` | | `node` | `@hono/node-server/serve-static` | The adapter is **lazy-loaded** on the first request to `/assets/*`, not at startup. This avoids runtime-specific import issues during testing or mixed environments. If the adapter fails to load (e.g., the package isn't installed), static serving is silently disabled and requests to `/assets/*` fall through to the 404 handler. ### Node Requirement When using `runtime: "node"`, make sure the Node adapter is installed: ```bash bun add @hono/node-server ``` The Bun adapter ships with Hono by default, so no extra install is needed for Bun. ## Middleware Order Static file serving runs after CORS and rate limiting, but before the main service endpoint: 1. CORS 2. Rate limiting 3. **Static file serving** (`/assets/*`) 4. `POST {baseUrl}/services` (RPC endpoint) 5. `GET /status` (health check) 6. 404 handler This means static file requests respect your CORS and rate limiting configuration. --- url: /nile/guide/basics/custom-routes.md --- # Custom Routes Nile's REST interface is a standard Hono app. After creating your server, you can add custom routes directly on the Hono instance for things Nile doesn't handle through the service/action model — webhooks, OAuth callbacks, health checks, or any traditional HTTP endpoint. ## Accessing the Hono App `createNileServer` returns a `NileServer` object with a `rest.app` property — a regular Hono instance: ```typescript import { createNileServer } from "@nilejs/nile"; const server = createNileServer({ name: "MyApp", services: [/* ... */], rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], }, }); const app = server.rest?.app; ``` ## Adding Routes Use any Hono method (`get`, `post`, `put`, `delete`, `all`) to register custom routes: ### Webhook Endpoint ```typescript app?.post("/webhooks/stripe", async (c) => { const signature = c.req.header("stripe-signature"); const body = await c.req.text(); // Verify and process the webhook const event = verifyStripeSignature(body, signature); if (!event) { return c.json({ error: "Invalid signature" }, 401); } await processStripeEvent(event); return c.json({ received: true }); }); ``` ### OAuth Callback ```typescript app?.get("/auth/callback", async (c) => { const code = c.req.query("code"); const state = c.req.query("state"); if (!code || !state) { return c.json({ error: "Missing code or state" }, 400); } const tokens = await exchangeCodeForTokens(code); // Set cookie, create session, etc. return c.redirect("/dashboard"); }); ``` ### Custom Health Check ```typescript app?.get("/health", async (c) => { const dbHealthy = await checkDatabaseConnection(); const cacheHealthy = await checkCacheConnection(); const healthy = dbHealthy && cacheHealthy; return c.json({ status: healthy ? "ok" : "degraded", checks: { database: dbHealthy ? "up" : "down", cache: cacheHealthy ? "up" : "down", }, }, healthy ? 200 : 503); }); ``` ## Using Nile Context in Custom Routes Access the shared `NileContext` from custom routes using `getContext`: ```typescript import { createNileServer, getContext } from "@nilejs/nile"; const server = createNileServer({ name: "MyApp", services: [/* ... */], resources: { database: db, logger }, rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], }, }); server.rest?.app.post("/webhooks/payment", async (c) => { const ctx = getContext(); const logger = ctx.resources?.logger; const database = ctx.resources?.database; const payload = await c.req.json(); logger?.info({ atFunction: "webhookHandler", message: "Payment webhook received", data: { eventType: payload.type }, }); // Use your database, cache, or any shared resource await database?.insert(payments).values({ eventId: payload.id, amount: payload.amount, }); return c.json({ processed: true }); }); ``` ## Adding Middleware You can also add Hono middleware to the app for custom routes: ```typescript // Apply to specific paths app?.use("/webhooks/*", async (c, next) => { const apiKey = c.req.header("x-api-key"); if (apiKey !== process.env.WEBHOOK_SECRET) { return c.json({ error: "Unauthorized" }, 401); } await next(); }); // Then register your webhook handlers app?.post("/webhooks/stripe", stripeHandler); app?.post("/webhooks/github", githubHandler); ``` ## Route Order Custom routes registered after `createNileServer` are added after Nile's built-in routes. The order is: 1. CORS middleware 2. Rate limiting middleware 3. Static file serving (`/assets/*`) 4. `POST {baseUrl}/services` (Nile RPC) 5. `GET /status` (if enabled) 6. **Your custom routes** 7. 404 handler Since Nile's 404 handler catches unmatched routes, register your custom routes **before** exporting the app for serving. ## Full Example ```typescript import { createNileServer, getContext } from "@nilejs/nile"; import { Ok } from "slang-ts"; const server = createNileServer({ name: "PaymentService", services: [ { name: "payments", description: "Payment processing", actions: [ { name: "list", description: "List recent payments", handler: () => Ok({ payments: [] }), }, ], }, ], resources: { database: db }, rest: { baseUrl: "/api/v1", allowedOrigins: ["http://localhost:3000"], enableStatus: true, }, }); const app = server.rest?.app; // Stripe webhook — outside the service/action model app?.post("/webhooks/stripe", async (c) => { const ctx = getContext(); const body = await c.req.json(); // Process webhook... return c.json({ received: true }); }); // OAuth callback app?.get("/auth/google/callback", async (c) => { const code = c.req.query("code"); // Exchange code, set session... return c.redirect("/"); }); export default app; ``` --- url: /nile/guide/basics/interacting.md --- # Interacting with Nile Nile exposes a single HTTP endpoint for all interactions. All requests are POST requests with a JSON body that specifies the `intent`, `service`, `action`, and `payload`. ## The Nile Client (Recommended) While you can interact with Nile using raw HTTP requests, we recommend using the `@nilejs/client` package for a better developer experience, type-safety, and graceful error handling. ```bash bun add @nilejs/client ``` ```typescript import { createNileClient } from "@nilejs/client"; import type { ServicePayloads } from "./generated/types"; const nile = createNileClient({ baseUrl: "/api" }); const { error, data } = await nile.invoke({ service: "tasks", action: "create", payload: { title: "Buy milk" } }); ``` See the [Client](/guide/basics/client) guide for the full API reference. ## The Single Endpoint ``` POST {baseUrl}/services ``` The default base URL is `/api`, so the full endpoint is typically: ``` POST /api/services ``` ## Request Format Every request follows this structure: ```json { "intent": "explore" | "execute" | "schema", "service": "serviceName", "action": "actionName", "payload": { ... } } ``` | Field | Type | Description | |-------|------|-------------| | `intent` | `string` | What you want to do: `explore`, `execute`, or `schema` | | `service` | `string` | The service name, or `"*"` for wildcard | | `action` | `string` | The action name, or `"*"` for wildcard | | `payload` | `object` | The input data for the action | ## Intents ### 1. Execute (`intent: "execute"`) Execute an action. This is the most common intent for running your business logic. In the Nile Client, this is called via `nile.invoke()`. ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "execute", "service": "tasks", "action": "create", "payload": { "title": "Buy milk", "status": "pending" } }' ``` **Response:** ```json { "status": true, "message": "Action 'tasks.create' executed", "data": { "task": { "id": "abc-123", "title": "Buy milk", "status": "pending" } } } ``` ### 2. Explore (`intent: "explore"`) Discover available services and actions. Use wildcards to explore. **List all services:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "explore", "service": "*", "action": "*", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "Available services", "data": [ { "name": "tasks", "description": "Task management operations", "meta": { "version": "1.0.0" }, "actions": ["create", "list", "get", "update", "delete"] }, { "name": "auth", "description": "Authentication service", "actions": ["login", "logout", "register"] } ] } ``` **List all actions in a service:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "explore", "service": "tasks", "action": "*", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "Actions for 'tasks'", "data": [ { "name": "create", "description": "Create a new task", "isProtected": false, "validation": true, "accessControl": [] }, { "name": "list", "description": "List all tasks", "isProtected": false, "validation": false, "accessControl": [] }, { "name": "get", "description": "Get a task by ID", "isProtected": true, "validation": true, "accessControl": [] } ] } ``` **Get details of a specific action:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "explore", "service": "tasks", "action": "create", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "Details for 'tasks.create'", "data": { "name": "create", "description": "Create a new task", "isProtected": false, "accessControl": null, "hooks": { "before": [], "after": [] }, "meta": null } } ``` ### 3. Schema (`intent: "schema"`) Get the Zod validation schemas as JSON Schema. Useful for generating type-safe clients. **Get all schemas:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "schema", "service": "*", "action": "*", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "All service schemas", "data": { "tasks": { "create": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1 }, "status": { "type": "string", "enum": ["pending", "in-progress", "done"] } }, "required": ["title"] }, "list": null, "get": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } }, "auth": { "login": { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string" } }, "required": ["email", "password"] } } } } ``` **Get schemas for a specific service:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "schema", "service": "tasks", "action": "*", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "Schemas for 'tasks'", "data": { "create": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1 }, "status": { "type": "string", "enum": ["pending", "in-progress", "done"] } }, "required": ["title"] }, "list": null, "get": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } } } ``` **Get schema for a specific action:** ```bash curl -X POST http://localhost:8000/api/services \ -H "Content-Type: application/json" \ -d '{ "intent": "schema", "service": "tasks", "action": "create", "payload": {} }' ``` **Response:** ```json { "status": true, "message": "Schema for 'tasks.create'", "data": { "create": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1 }, "status": { "type": "string", "enum": ["pending", "in-progress", "done"] } }, "required": ["title"] } } } ``` ## Response Format All responses follow a consistent structure: ```typescript { status: boolean; // true for success, false for error message: string; // human-readable message data: { // the actual response data error_id?: string; // present on errors [key: string]: any; } } ``` **Success response:** ```json { "status": true, "message": "Action executed successfully", "data": { ... } } ``` **Error response:** ```json { "status": false, "message": "Validation failed: Title is required", "data": {} } ``` ## Error Handling Nile uses a Result pattern internally. All errors are returned in the response without throwing HTTP exceptions: | HTTP Status | Meaning | |-------------|---------| | `200` | Success | | `400` | Bad request (invalid JSON, missing fields, validation errors) | | `404` | Service or action not found | ## Health Check If enabled in config, you can check server health: ``` GET /status ``` ```json { "status": true, "message": "my-app is running", "data": {} } ``` --- url: /nile/guide/basics/client.md --- # Nile Client The `@nilejs/client` package is a standalone, type-safe client for interacting with a Nile backend from any JavaScript environment (browser, server, or edge). ## Installation ```bash bun add @nilejs/client ``` ## Creating a Client ```typescript import { createNileClient } from "@nilejs/client"; const nile = createNileClient({ baseUrl: "http://localhost:8000/api", credentials: "include", }); ``` ### Configuration | Option | Type | Default | Description | |--------|------|---------|-------------| | `baseUrl` | `string` | *required* | Base URL of your Nile API | | `credentials` | `"include" \| "omit" \| "same-origin"` | - | Fetch credentials mode | | `headers` | `Record` | - | Global headers for every request | | `timeout` | `number` | `30000` | Default request timeout in ms | ## Invoking Actions The primary method is `invoke`, which sends an `execute` intent to the Nile backend: ```typescript const { error, data } = await nile.invoke({ service: "tasks", action: "create", payload: { title: "Buy milk" }, }); if (error) { console.error("Failed:", error); } else { console.log("Created:", data); } ``` Every method returns a `ClientResult`: ```typescript { error: string | null; // error message, or null on success data: T | null; // response data, or null on failure } ``` This is the Result pattern. The client never throws exceptions for expected failures. Network errors, timeouts, and server validation errors are all returned in the `error` field. ## Type-Safe Payloads For full compile-time type checking, generate types using the Nile CLI and pass them as a generic: ```bash bun run gen schema --output ./src/generated ``` ```typescript import { createNileClient } from "@nilejs/client"; import type { ServicePayloads } from "./generated/types"; const nile = createNileClient({ baseUrl: "/api", }); // TypeScript now enforces valid service names, action names, and payload shapes await nile.invoke({ service: "tasks", // autocomplete from your actual services action: "create", // autocomplete from actions in "tasks" payload: { // type-checked against your Zod schema title: "Buy milk", }, }); ``` If you pass an invalid service, action, or payload shape, TypeScript will catch it at compile time. ## Discovery Use `explore` to discover available services and actions at runtime: ```typescript // List all services const { data: services } = await nile.explore({ service: "*", action: "*", }); // List actions in a specific service const { data: actions } = await nile.explore({ service: "tasks", action: "*", }); // Get details for a specific action const { data: details } = await nile.explore({ service: "tasks", action: "create", }); ``` ## Schema Retrieval Use `schema` to fetch Zod validation schemas as JSON Schema, useful for dynamic form generation or runtime validation: ```typescript // Get schemas for all actions in a service const { data: schemas } = await nile.schema({ service: "tasks", action: "*", }); // Get schema for a specific action const { data: createSchema } = await nile.schema({ service: "tasks", action: "create", }); ``` ## Per-Request Options All methods accept optional `timeout` and `headers` overrides: ```typescript const { error, data } = await nile.invoke({ service: "tasks", action: "create", payload: { title: "Urgent task" }, timeout: 5000, headers: { Authorization: "Bearer my-token", }, }); ``` ## Error Handling The client handles three categories of errors: | Category | `error` value | `data` value | |----------|--------------|-------------| | Network failure | Error message (e.g., `"Failed to fetch"`) | `null` | | Timeout | `"Request timed out"` | `null` | | Server error | Server's error message | Server's error data (if any) | | Success | `null` | Response data | ```typescript const { error, data } = await nile.invoke({ service: "tasks", action: "create", payload: { title: "" }, // invalid: title is required to be non-empty }); if (error) { // error = "Validation failed: title - String must contain at least 1 character(s)" // data may contain additional error context from the server } ``` ## How It Works The client is a thin wrapper around `fetch` that speaks the Nile protocol: 1. All requests go to `POST {baseUrl}/services` 2. The request body contains `{ intent, service, action, payload }` 3. The response follows `{ status, message, data }` 4. The client maps this into `{ error, data }` for consumption The client has zero runtime dependencies. It uses an internal `safeTry` utility for crash-safe async operations instead of try/catch. --- url: /nile/guide/internals/server.md --- # Nile Server **Type:** Reference / Specification **Path:** `nile/` ## 1. Purpose The Nile Server module provides the top-level factory for bootstrapping a Nile application. `createNileServer` is the single entry point developers use to wire together the Action Engine, shared context, and interface layers (REST, and later WebSocket/RPC). ### 1.1 Responsibilities - **Bootstrapping:** Create and connect the Action Engine, `NileContext`, and REST interface from a single `ServerConfig` - **Context ownership:** Create a single `NileContext` instance shared across all interfaces - **Context access:** Export `getContext()` to retrieve the runtime context from anywhere within a request scope - **Lifecycle:** Execute `onBoot` hooks after initialization with crash safety - **Diagnostics:** Route diagnostic output through `createDiagnosticsLog` from `utils/diagnostics-log.ts`, which checks `resources.logger` first and falls back to `console.log`. See `docs/internals/logging.md` section 7. ### 1.2 Non-Goals - **Transport logic:** The server module does not handle HTTP routing, CORS, or request parsing. That is the REST layer's responsibility. - **Engine internals:** The server does not manage action execution or hook pipelines. It delegates to the engine. ## 2. `createNileServer` **Path:** `nile/server.ts` ```typescript import { createNileServer } from "@nilejs/nile"; const server = createNileServer({ serverName: "my-app", services: [/* ... */], rest: { baseUrl: "/api", allowedOrigins: ["http://localhost:8000"], enableStatus: true, }, }); ``` ### 2.1 Initialization Sequence 1. **Validate** — Throws immediately if `config.services` is empty 2. **Create `NileContext`** — Single instance with `config.resources` attached 3. **Create Engine** — Passes `services`, `diagnostics`, and global hook handlers 4. **Log services table** — When `config.logServices` is `true`, prints a `console.table` of registered services (name, description, actions). Always prints — not gated by `diagnostics` 5. **Create REST app** — Only if `config.rest` is provided. Passes engine, context, `serverName`, and `runtime` (defaults to `"bun"`) 6. **Print REST endpoint URLs** — When REST is configured, prints `POST http://host:port/baseUrl/services` and optionally `GET http://host:port/status` via `console.log`. Uses `rest.host` (default `"localhost"`) and `rest.port` (default `8000`) 7. **Run `onBoot`** — Fire-and-forget async IIFE. Failures are logged via `console.error` but do not crash the server ### 2.2 Return Value (`NileServer`) ```typescript { config: ServerConfig; engine: Engine; context: NileContext; rest?: { app: Hono; config: RestConfig }; } ``` - `rest` is only present when `config.rest` was provided - `engine` provides direct access to `getServices`, `getServiceActions`, `getAction`, `executeAction` - `context` is the shared `NileContext` passed to all layers ## 3. `ServerConfig` ```typescript { serverName: string; runtime?: ServerRuntime; // "bun" | "node", defaults to "bun" services: Services; // required, at least one diagnostics?: boolean; // default: false logServices?: boolean; // default: true, print services table via console.table resources?: Resources; // logger, database, cache, custom keys rest?: RestConfig; websocket?: Record; // placeholder, not yet implemented rpc?: Record; // placeholder, not yet implemented onBeforeActionHandler?: BeforeActionHandler; onAfterActionHandler?: AfterActionHandler; onBoot?: { fn: (context: NileContext) => Promise | void; }; } ``` - `runtime` lives only on `ServerConfig` and is piped to `createRestApp` as a parameter. It is not duplicated onto `RestConfig`. - `services` is required. An empty array throws at initialization. - `diagnostics` defaults to `false`. When enabled, internal modules emit diagnostic output through `createDiagnosticsLog`. - `logServices` defaults to `true`. Prints a `console.table` of registered services (Service, Description, Actions count). Not gated by `diagnostics` — set `logServices: false` to suppress. - When REST is configured, endpoint URLs are always printed via `console.log` using `rest.host` (default `"localhost"`) and `rest.port` (default `8000`). ## 4. `NileContext` **Path:** `nile/nile.ts` **Factory:** `createNileContext(params?)` The context is a singleton per server. It carries interface-specific data, hook execution state, session storage, and a general-purpose key-value store. It supports an optional `TDB` generic to provide type safety for the database resource. ### 4.1 Key-Value Store ```typescript context.set("tenant", { id: "abc" }); const tenant = context.get<{ id: string }>("tenant"); ``` `_store` is a `Map` exposed as readonly. Use `get`/`set` methods for access. ### 4.2 Sessions Each `NileContext` owns its own session store. Multiple server instances do not share session state. ```typescript context.setSession("rest", { userId: "123", token: "abc" }); const session = context.getSession("rest"); // { userId: "123", token: "abc" } ``` Session keys are `"rest" | "ws" | "rpc"`, matching the interface types. ### 4.3 Hook Context `hookContext` tracks the lifecycle of a single action execution. It is reset at the start of each `executeAction` call via `resetHookContext(actionName, input)`. ```typescript context.hookContext.actionName; // current action context.hookContext.state; // mutable key-value shared between hooks context.hookContext.log; // { before: HookLogEntry[], after: HookLogEntry[] } ``` Mutation methods: `updateHookState`, `addHookLog`, `setHookError`, `setHookOutput`. ### 4.4 Interface Contexts ```typescript context.rest // HonoContext (readonly, set at creation) context.ws // WebSocketContext (readonly) context.rpc // RPCContext (readonly) ``` These are set once during `createNileContext` via the `interfaceContext` parameter. The REST layer creates a fresh context per request with the Hono context attached. ### 4.5 Resources ```typescript context.resources?.logger; context.resources?.database; // typed as TDB context.resources?.cache; ``` Extensible via index signature. Passed through from `ServerConfig.resources`. The `database` field is typed as `TDB` (defaulting to `unknown`). ## 5. Key Types ### 5.1 `BeforeActionHandler` Global hook that runs before every action. Returns a `Result` — `Err` halts the pipeline. ```typescript type BeforeActionHandler = (params: { nileContext: NileContext; action: Action; payload: unknown; }) => Result; ``` ### 5.2 `AfterActionHandler` Global hook that runs after every action. Receives the action result and can transform it. ```typescript type AfterActionHandler = (params: { nileContext: NileContext; action: Action; payload: unknown; result: Result; }) => Result; ``` ### 5.3 `Sessions` ```typescript type Sessions = { rest?: Record; ws?: Record; rpc?: Record; }; ``` ### 5.4 `Resources` ```typescript interface NileLogger { info: (input: { atFunction: string; message: string; data?: unknown }) => string; warn: (input: { atFunction: string; message: string; data?: unknown }) => string; error: (input: { atFunction: string; message: string; data?: unknown }) => string; } type Resources = { logger?: NileLogger; database?: TDB; cache?: unknown; [key: string]: unknown; }; ``` The `logger` field accepts a `NileLogger` — the return type of `createLogger` from the logging module. This enables `handleError` and `createDiagnosticsLog` to log through the same logger instance. ## 6. Constraints - **One context per server** — `createNileContext` is called once in `createNileServer`. All interfaces share this instance. - **Generic Database Support** — To avoid generic leakage into the core engine, the database type `TDB` is only present in `NileContext` and `Resources`. High-level components (Engine, REST) use `unknown`. - **`onBoot` is fire-and-forget** — It runs in an async IIFE and is not awaited. Errors are caught by `safeTry` and logged to `console.error`. - **Runtime default** — If `config.runtime` is omitted, it defaults to `"bun"`. This affects static file serving and runtime-specific behavior. - **No dynamic service injection** — Services are fixed at boot time. Adding services after initialization is not supported. ## 7. Failure Modes - **Empty services** — `createNileServer` throws immediately with a descriptive error - **`onBoot` crash** — Caught by `safeTry`, logged to `console.error`, does not prevent server from starting - **Missing resources** — `resources` is optional. Diagnostics fall back to `console.log` when `resources.logger` is absent (handled by `createDiagnosticsLog`) ## 8. `getContext` **Path:** `nile/server.ts` Exported function that retrieves the runtime `NileContext` from anywhere within a request scope. It accepts an optional `TDB` generic for type-safe database access. The context is stored in a module-level variable set during `createNileServer` initialization. ```typescript import { getContext } from "@nilejs/nile"; // Type-safe access to your database const ctx = getContext(); // Access resources, sessions, etc. const db = ctx.resources?.database; // typed as MyDatabaseType ctx.resources?.logger; ctx.getSession("rest"); ctx.set("user", { id: "123" }); ``` ### 8.1 Usage Pattern `getContext` is designed to be called from action handlers or utility functions that need access to the context but don't receive it as a parameter: ```typescript // In an action handler const handler = async (data, ctx) => { // Both ctx and getContext() work const userId = ctx.get("userId") ?? getContext().get("userId"); return Ok({ userId }); }; ``` ### 8.2 Constraints - **Must be called after server initialization** — `getContext` throws if called before `createNileServer` has run - **Must be called within a request scope** — The context is set once at server boot, not per-request. For per-request isolation, use the context passed to action handlers ### 8.3 Failure Modes - **Called before server boot** — Throws `"getContext: Server not initialized. Call createNileServer first."` --- url: /nile/guide/internals/db/index.md --- # Database Utilities ## Purpose Provides utilities for integrating with Drizzle ORM, including schema generation, transaction management, and a recommended **model file** pattern for organizing database operations. ## Constraints - Requires `drizzle-orm` and `drizzle-zod` as peer dependencies. - Intended for use with Drizzle-compatible databases (PostgreSQL, SQLite, etc.). ## 1. Folder Organization Nile recommends a dedicated `db/` directory for all database concerns, with a `models/` subdirectory for data access functions: ``` src/ ├── db/ │ ├── client.ts # Database client setup (connection, ORM wrapper) │ ├── schema.ts # Drizzle table definitions │ ├── types.ts # Inferred types from schema │ ├── index.ts # Barrel exports │ └── models/ │ ├── tasks.ts # Model functions for the tasks table │ ├── users.ts # Model functions for the users table │ └── index.ts # Barrel re-exports all models ├── services/ │ └── ... # Action handlers import from @/db/models └── index.ts # Server entry point ``` Each layer has a clear responsibility: | File | Responsibility | |------|---------------| | `client.ts` | Initialize the database connection and export the `db` instance | | `schema.ts` | Define Drizzle table schemas (columns, types, defaults) | | `types.ts` | Infer TypeScript types from the schema (`Task`, `NewTask`, etc.) | | `models/*.ts` | CRUD functions that validate, query, and return `Result` | | `index.ts` | Barrel files for clean imports | ### Example: client.ts ```typescript import { PGlite } from "@electric-sql/pglite"; import { drizzle } from "drizzle-orm/pglite"; import { tasks } from "./schema"; const DATA_DIR = `${process.cwd()}/data`; Bun.spawnSync(["mkdir", "-p", DATA_DIR]); export const pglite = new PGlite(DATA_DIR); export const db = drizzle(pglite, { schema: { tasks } }); ``` ### Example: schema.ts ```typescript import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; export const tasks = pgTable("tasks", { id: uuid("id").defaultRandom().primaryKey(), title: text("title").notNull(), description: text("description"), status: text("status", { enum: ["pending", "in-progress", "done"] }) .notNull() .default("pending"), created_at: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updated_at: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow() .$onUpdate(() => new Date()), }); ``` ### Example: types.ts ```typescript import type { tasks } from "./schema"; export type Task = typeof tasks.$inferSelect; export type NewTask = typeof tasks.$inferInsert; ``` ## 2. Model Files Model files are the core pattern for database operations in Nile. Each model file contains functions that interact with a single table, using consistent patterns for validation, error handling, and result returns. ### Key Principles - **One model file per table** — `models/tasks.ts` handles all task operations - **All model functions return `Result`** — using `Ok()` for success and `handleError()` for failures - **Null checks belong in model files** — not in action handlers. If a row is not found, the model returns `handleError(...)`, not the handler. - **Use `safeTry` from `slang-ts`** for all database calls — check `result.isOk` / `result.isErr` and access `result.value` / `result.error` - **Use `handleError` from `@nilejs/nile`** for all error returns — this logs the error and returns an `Err` with a traceable log ID - **Use `getZodSchema`** for input validation before writes - **Accept `dbx` parameter** for transaction support via `DBX` ### Full Model File Example ```typescript // db/models/tasks.ts import { createTransactionVariant, type DBX, getZodSchema, handleError, } from "@nilejs/nile"; import { desc, eq } from "drizzle-orm"; import { Ok, safeTry } from "slang-ts"; import { db } from "@/db/client"; import { tasks } from "@/db/schema"; import type { NewTask, Task } from "@/db/types"; const parsedSchema = getZodSchema(tasks); /** Create a new task with validation */ export const createTask = async ({ task, dbx = db, }: { task: NewTask; dbx?: DBX; }) => { const parsed = parsedSchema.insert.safeParse(task); if (!parsed.success) { return handleError({ message: "Invalid task data", data: { errors: parsed.error }, atFunction: "createTask", }); } const result = await safeTry(() => { return dbx.insert(tasks).values(task).returning(); }); if (result.isErr) { return handleError({ message: "Error creating task", data: { task, error: result.error }, atFunction: "createTask", }); } const data = result.value?.[0] ?? null; if (!data) { return handleError({ message: "Task creation returned no data", data: { task }, atFunction: "createTask", }); } return Ok(data); }; // Transaction-aware variant — automatically wraps in db.transaction(...) export const createTaskTx = createTransactionVariant(createTask); /** Get a single task by ID */ export const getTaskById = async (taskId: string) => { const result = await safeTry(() => { return db.query.tasks.findFirst({ where: eq(tasks.id, taskId) }); }); if (result.isErr) { return handleError({ message: "Error getting task", data: { taskId, error: result.error }, atFunction: "getTaskById", }); } // Null check in the model, not the handler if (!result.value) { return handleError({ message: "Task not found", data: { taskId }, atFunction: "getTaskById", }); } return Ok(result.value); }; /** Update an existing task by ID */ export const updateTask = async ({ taskId, task, dbx = db, }: { taskId: string; task: Partial; dbx?: DBX; }) => { const parsed = parsedSchema.update.safeParse(task); if (!parsed.success) { return handleError({ message: "Invalid task data", data: { errors: parsed.error }, atFunction: "updateTask", }); } const result = await safeTry(() => { return dbx.update(tasks).set(task).where(eq(tasks.id, taskId)).returning(); }); if (result.isErr) { return handleError({ message: "Error updating task", data: { taskId, task, error: result.error }, atFunction: "updateTask", }); } const data = result.value?.[0] ?? null; if (!data) { return handleError({ message: "Task not found", data: { taskId }, atFunction: "updateTask", }); } return Ok(data); }; export const updateTaskTx = createTransactionVariant(updateTask); /** Delete a task by ID */ export const deleteTask = async (taskId: string) => { const result = await safeTry(() => { return db.delete(tasks).where(eq(tasks.id, taskId)).returning(); }); if (result.isErr) { return handleError({ message: "Error deleting task", data: { taskId, error: result.error }, atFunction: "deleteTask", }); } const data = result.value?.[0] ?? null; if (!data) { return handleError({ message: "Task not found", data: { taskId }, atFunction: "deleteTask", }); } return Ok(data); }; /** List all tasks, newest first */ export const getAllTasks = async () => { const result = await safeTry(() => { return db.select().from(tasks).orderBy(desc(tasks.created_at)); }); if (result.isErr) { return handleError({ message: "Error getting all tasks", data: { error: result.error }, atFunction: "getAllTasks", }); } return Ok(result.value ?? []); }; ``` ### How Action Handlers Use Models Action handlers stay thin — they call the model function and forward the result: ```typescript // services/tasks/create.ts import { type Action, createAction } from "@nilejs/nile"; import { Err, Ok } from "slang-ts"; import z from "zod"; import { createTask } from "@/db/models"; const createTaskSchema = z.object({ title: z.string().min(1, "Title is required"), description: z.string().optional().default(""), status: z.enum(["pending", "in-progress", "done"]).optional().default("pending"), }); const createTaskHandler = async (data: Record) => { const result = await createTask({ task: { title: data.title as string, description: (data.description as string) ?? "", status: (data.status as "pending" | "in-progress" | "done") ?? "pending", }, }); if (result.isErr) { return Err(result.error); } return Ok({ task: result.value }); }; export const createTaskAction: Action = createAction({ name: "create", description: "Create a new task", handler: createTaskHandler, validation: createTaskSchema, }); ``` ## 3. handleError The `handleError` utility is the standard way to return errors from model functions. It logs the error and returns a traceable `Err` result. ### Usage ```typescript import { handleError } from "@nilejs/nile"; // In a model function: if (!result.value) { return handleError({ message: "Task not found", data: { taskId }, atFunction: "getTaskById", }); } ``` ### Behavior 1. **Logger resolution** — uses the explicit `logger` param if provided, otherwise resolves from `getContext().resources.logger` 2. **Caller inference** — parses `Error().stack` to detect the calling function name. Override with `atFunction` when needed (arrow functions, callbacks) 3. **Logging** — calls `logger.error({ atFunction, message, data })` and receives a `log_id` back 4. **Return** — returns `Err("[log_id] message")`, making every error traceable in logs ### Interface ```typescript interface HandleErrorParams { message: string; // Human-readable error description data?: unknown; // Structured context data for debugging logger?: NileLogger; // Explicit logger (optional — resolved from context) atFunction?: string; // Override auto-inferred caller name } ``` ### Return Type ```typescript ErrType & ResultMethods ``` Always returns an `Err` variant. Compatible with any `Result` union, so model functions can return `Ok(data)` or `handleError(...)` from the same function. ### Why handleError Instead of Err() - **Traceability** — every error gets a unique `log_id` for log correlation - **Automatic logging** — errors are logged at the error site, not somewhere upstream - **Context-aware** — resolves the logger from the Nile context without explicit imports - **Consistent** — all errors follow the same `[logId] message` format ## 4. Key Types ### DBX ```typescript type DBX = TDB | Parameters[0]>[0]; ``` A union type representing either a root database instance or a transaction pointer. Used in model function signatures to accept both: ```typescript export const createTask = async ({ task, dbx = db, // defaults to root db, but accepts a transaction }: { task: NewTask; dbx?: DBX; }) => { ... }; ``` ### DBParams ```typescript interface DBParams { dbx?: DBX; } ``` Standard interface for functions that accept an optional database or transaction parameter. ### TableSchemas ```typescript interface TableSchemas { insert: ZodObject; update: ZodObject; select: ZodObject; } ``` Object containing Zod schemas for insert, update, and select operations. Generated by `getZodSchema`. ## 5. Utilities ### getZodSchema Generates Zod validation schemas from a Drizzle table definition: ```typescript import { getZodSchema } from "@nilejs/nile"; import { tasks } from "@/db/schema"; const parsedSchema = getZodSchema(tasks); // parsedSchema.insert — for validating new records // parsedSchema.update — for validating partial updates // parsedSchema.select — for validating query results ``` Call this once per table at module scope and reuse across model functions. ### getContext Retrieves the shared Nile context with type-safe database access: ```typescript import { getContext } from "@nilejs/nile"; const handler = async (data: any) => { const db = getContext().resources?.database; if (!db) return Err("Database not found"); const results = await db.select().from(users); return Ok(results); }; ``` ### createTransactionVariant Creates a transaction-aware wrapper around a model function. When called, it automatically wraps the operation in `db.transaction(...)` and triggers rollback if the function returns `Err`. ```typescript import { createTransactionVariant } from "@nilejs/nile"; // Standard model function const createTask = async ({ task, dbx = db }: { task: NewTask; dbx?: DBX }) => { // ... validate, insert, return Ok or handleError }; // Transaction variant — wraps in db.transaction automatically const createTaskTx = createTransactionVariant(createTask); // Usage: automatically runs inside a transaction const result = await createTaskTx({ task: data, dbx: db }); ``` **Behavior:** - Wraps the function call inside `dbx.transaction(tx => fn({ ...params, dbx: tx }))` - If the inner function returns `Err`, the transaction wrapper throws to trigger rollback - The thrown error is caught and the original `Err` is returned to the caller ## 6. Putting It All Together A complete Nile project with database integration follows this structure: ``` my-app/ ├── src/ │ ├── index.ts # Server entry point │ ├── db/ │ │ ├── client.ts # DB connection setup │ │ ├── schema.ts # Drizzle table definitions │ │ ├── types.ts # Inferred types │ │ ├── index.ts # Barrel exports │ │ └── models/ │ │ ├── tasks.ts # CRUD for tasks table │ │ ├── users.ts # CRUD for users table │ │ └── index.ts # Barrel re-exports │ └── services/ │ ├── services.config.ts # Service definitions │ └── tasks/ │ ├── create.ts # Action: calls createTask model │ ├── list.ts # Action: calls getAllTasks model │ ├── get.ts # Action: calls getTaskById model │ ├── update.ts # Action: calls updateTask model │ └── delete.ts # Action: calls deleteTask model ├── package.json ├── tsconfig.json # Path alias: @/* → ./src/* └── drizzle.config.ts # Drizzle Kit config ``` The data flows in one direction: ``` Request → Action Handler → Model Function → Database ↑ ↓ Ok/Err Ok/handleError ``` Action handlers never touch the database directly. They call model functions, which validate inputs, run queries via `safeTry`, handle null checks, and return typed `Result` values. ## 7. Failure Modes - **`getZodSchema`** — throws if passed a relation schema instead of a table schema - **`createTransactionVariant`** — throws when the wrapped function returns `Err` (intentional, triggers rollback) - **`handleError`** — throws if no logger is available (neither explicit nor on context) --- url: /nile/guide/internals/db/create-model.md --- # createModel CRUD model factory for Drizzle tables. Replaces repetitive `safeTry` + `handleError` + null-check boilerplate with a single function call. ## Signature ```typescript import { createModel } from '@nilejs/nile'; const taskModel = createModel(table, options); ``` ```typescript function createModel( table: TTable, options: ModelOptions ): ModelOperations ``` ## Inputs ### `table` (required) A Drizzle table definition created via `pgTable`, `sqliteTable`, etc. ```typescript import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; export const tasks = pgTable('tasks', { id: uuid('id').primaryKey().defaultRandom(), title: text('title').notNull(), description: text('description'), status: text('status', { enum: ['pending', 'in-progress', 'done'] }) .notNull() .default('pending'), created_at: timestamp('created_at', { withTimezone: true }) .notNull() .defaultNow(), }); ``` Must be a real Drizzle table object — it carries internal Symbol-keyed metadata that `getZodSchema` (via `drizzle-zod`) needs to auto-generate validation schemas. Plain objects will fail. ### `options: ModelOptions` (required) ```typescript interface ModelOptions { db?: TDB; name: string; cursorColumn?: string; } ``` #### `options.name` (required) Human-readable entity name. Used in error messages and `handleError` attribution. ```typescript createModel(tasks, { name: 'task' }); // Error messages: "Task not found", "Error creating task" // atFunction values: "task.create", "task.findById", etc. ``` Auto-capitalized for user-facing messages: `"task"` → `"Task not found"`. #### `options.db` (optional) Explicit Drizzle database instance. Accepts any Drizzle driver (Neon, PGLite, etc.). ```typescript import { db } from './client'; // Explicit — db is fixed at factory creation time const model = createModel(tasks, { db, name: 'task' }); // Omitted — db resolved from Nile context at each method call const model = createModel(tasks, { name: 'task' }); ``` When omitted, each method call resolves the db via `getContext().resources.database`. This supports request-scoped database access in Nile's context system. Throws immediately if neither explicit db nor context db is available: `"createModel: No database available."`. #### `options.cursorColumn` (optional, default: `"id"`) Default column name for cursor-based pagination. Can be overridden per-query. ```typescript // Model-level: paginate by created_at by default const model = createModel(tasks, { db, name: 'task', cursorColumn: 'created_at' }); // Per-query override await model.findPaginated({ limit: 20, cursor: 'abc', cursorColumn: 'id' }); ``` ## Output Returns `ModelOperations` where type parameters are inferred from the Drizzle table: - `TSelect` — row type from select queries (`table.$inferSelect`) - `TInsert` — data type for inserts (`table.$inferInsert`) - `TDB` — database type for transaction support All async methods return `Result` from `slang-ts`. ### CRUD Methods #### `create({ data, dbx? })` Insert a new record with auto-validation. ```typescript create(params: { data: TInsert; // Validated against auto-generated insert schema dbx?: DBX; // Optional transaction pointer }): Promise> ``` Returns `Err` if validation fails, insert returns empty, or db throws. #### `createTx({ data, dbx? })` Same as `create`, wrapped in a database transaction via `createTransactionVariant`. Rolls back on `Err`. #### `findById(id)` Find a single record by UUID. ```typescript findById(id: string): Promise> ``` Returns `Err("{Name} not found")` when no row matches. #### `update({ id, data, dbx? })` Update a record by UUID with auto-validation. ```typescript update(params: { id: string; data: Partial; // Validated against auto-generated update schema dbx?: DBX; }): Promise> ``` Returns `Err("{Name} not found")` when no row matches the id. #### `updateTx({ id, data, dbx? })` Same as `update`, wrapped in a database transaction. Rolls back on `Err`. #### `delete(id)` Delete a record by UUID, returns the deleted row. ```typescript delete(id: string): Promise> ``` #### `findAll()` Get all records. Auto-orders by `created_at` or `createdAt` descending when that column exists on the table. ```typescript findAll(): Promise> ``` Returns `Ok([])` for empty tables — not an error. #### `findPaginated(options?)` Two modes, determined by which options are passed. **Offset mode** (default — no `cursor` provided): ```typescript await model.findPaginated({ limit: 20, offset: 0 }); ``` ```typescript interface OffsetPaginationOptions { limit?: number; // Default: 50 offset?: number; // Default: 0 } ``` Returns: ```typescript interface OffsetPage { items: T[]; total: number; // Total count across all pages hasMore: boolean; // offset + items.length < total } ``` **Cursor mode** (when `cursor` is provided): ```typescript await model.findPaginated({ limit: 20, cursor: 'abc-123', cursorColumn: 'created_at' }); ``` ```typescript interface CursorPaginationOptions { limit?: number; // Default: 50 cursor: string; // Value to paginate from cursorColumn?: string; // Overrides model-level default } ``` Returns: ```typescript interface CursorPage { items: T[]; nextCursor: string | null; // Pass as cursor for next page hasMore: boolean; } ``` Uses `lt()` on the cursor column with `desc` ordering. Fetches `limit + 1` rows to determine `hasMore` without a separate count query. ### Escape Hatches #### `table` The original Drizzle table. Use for custom queries beyond CRUD. ```typescript const db = getContext().resources.database; const active = await db.select().from(model.table).where(eq(model.table.status, 'active')); ``` #### `schemas` Auto-generated Zod schemas from the Drizzle table via `getZodSchema`. ```typescript model.schemas.insert // For validating create data model.schemas.update // For validating update data model.schemas.select // For validating query results ``` ## Example ### Model definition ```typescript // db/models/tasks.ts import { createModel } from '@nilejs/nile'; import { tasks } from '../schema'; import { db } from '../client'; export const taskModel = createModel(tasks, { db, name: 'task' }); ``` ### Usage in action handlers ```typescript // services/tasks/create.ts import { taskModel } from '../../db/models'; const handler = async (data: Record) => { const result = await taskModel.create({ data: { title: data.title as string }, }); if (result.isErr) return Err(result.error); return Ok({ task: result.value }); }; ``` ```typescript // services/tasks/list.ts const handler = async () => { return taskModel.findAll(); }; ``` ```typescript // services/tasks/get.ts const handler = async (data: Record) => { return taskModel.findById(data.taskId as string); }; ``` ### Custom queries via escape hatch ```typescript import { eq } from 'drizzle-orm'; const db = getContext().resources.database; const pending = await db .select() .from(taskModel.table) .where(eq(taskModel.table.status, 'pending')); ``` ## Internals ### DB Resolution ``` method call → options.db exists? → use it ↓ no getContext().resources.database exists? → use it ↓ no throw "No database available" ``` Resolution is lazy (at call time, not factory creation) so context-based apps work correctly. ### Validation Flow ``` create/update → schemas.insert/update.safeParse(data) ↓ fails → handleError("Invalid {name} data") ↓ passes → execute db query ``` ### Error Handling All errors go through `handleError`: 1. Resolves logger from explicit param or Nile context 2. Logs with `atFunction` attribution (e.g. `task.create`) 3. Returns `Err("[logId] message")` with traceable log ID ### Auto-Ordering `findAll` and offset `findPaginated` auto-detect timestamp columns: 1. Checks for `created_at` on the table 2. Falls back to `createdAt` 3. No ordering applied if neither exists ## Failure Modes | Scenario | Behavior | |---|---| | No db available (explicit or context) | Throws immediately — developer config error | | Validation fails | Returns `Err` with validation details | | DB query throws | Returns `Err` via `handleError` | | Row not found (findById, update, delete) | Returns `Err("{Name} not found")` | | Insert returns empty | Returns `Err("{Name} creation returned no data")` | | Invalid cursor column | Returns `Err("Cursor column '{col}' does not exist on {name} table")` | ## Pairing with createServices `createModel` eliminates model boilerplate. Pair it with `createServices` to eliminate the service/action layer boilerplate too. Together they reduce a full CRUD service from ~250 lines across 7 files to ~40 lines across 3 files. ### The pattern ``` Schema (pgTable) └─ createModel → CRUD model (1 line) └─ Service config (with direct action arrays) → CRUD actions (5 action definitions) └─ done ``` ### Step 1: Model — one line ```typescript // db/models/tasks.ts import { createModel } from '@nilejs/nile'; import { tasks } from '../schema'; import { db } from '../client'; export const taskModel = createModel(tasks, { db, name: 'task' }); ``` ### Step 2: Actions — each handler calls one model method ```typescript // services/tasks/create.ts import { createAction, type Action } from '@nilejs/nile'; import { Err, Ok } from 'slang-ts'; import z from 'zod'; import { taskModel } from '@/db/models'; const createTaskSchema = z.object({ title: z.string().min(1, 'Title is required'), description: z.string().optional().default(''), status: z.enum(['pending', 'in-progress', 'done']).optional().default('pending'), }); const createTaskHandler = async (data: Record) => { const result = await taskModel.create({ data: { title: data.title as string, description: (data.description as string) ?? '', status: (data.status as 'pending' | 'in-progress' | 'done') ?? 'pending', }, }); if (result.isErr) return Err(result.error); return Ok({ task: result.value }); }; export const createTaskAction: Action = createAction({ name: 'create', description: 'Create a new task', handler: createTaskHandler, validation: createTaskSchema, }); ``` ```typescript // services/tasks/list.ts import { createAction, type Action } from '@nilejs/nile'; import { Err, Ok } from 'slang-ts'; import { taskModel } from '@/db/models'; const listTasksHandler = async () => { const result = await taskModel.findAll(); if (result.isErr) return Err(result.error); return Ok({ tasks: result.value }); }; export const listTaskAction: Action = createAction({ name: 'list', description: 'List all tasks', handler: listTasksHandler, }); ``` ```typescript // services/tasks/get.ts const getTaskHandler = async (data: Record) => { const result = await taskModel.findById(data.id as string); if (result.isErr) return Err(result.error); return Ok({ task: result.value }); }; ``` ```typescript // services/tasks/update.ts const updateTaskHandler = async (data: Record) => { const { id, ...updates } = data; const result = await taskModel.update({ id: id as string, data: updates }); if (result.isErr) return Err(result.error); return Ok({ task: result.value }); }; ``` ```typescript // services/tasks/delete.ts const deleteTaskHandler = async (data: Record) => { const result = await taskModel.delete(data.id as string); if (result.isErr) return Err(result.error); return Ok({ deleted: true, id: data.id }); }; ``` ### Step 3: Wire into services ```typescript // services/services.config.ts import { createServices, type Services } from '@nilejs/nile'; import { createTaskAction } from './tasks/create'; import { deleteTaskAction } from './tasks/delete'; import { getTaskAction } from './tasks/get'; import { listTaskAction } from './tasks/list'; import { updateTaskAction } from './tasks/update'; export const services: Services = createServices([ { name: 'tasks', description: 'Task management with CRUD operations', actions: [ createTaskAction, listTaskAction, getTaskAction, updateTaskAction, deleteTaskAction, ], }, ]); ``` ### What each layer handles | Concern | Handled by | |---|---| | Table schema, columns, defaults | `pgTable` (Drizzle) | | Validation, CRUD queries, error handling, transactions | `createModel` | | Input schemas, business logic wrapping, response shaping | `createAction` per action | | Type-safe action grouping, service registration | `createServices` with direct action arrays | | Routing, execution pipeline, hooks | Nile engine (automatic) | ### When to go beyond this pattern The `createModel` + `createServices` pattern covers standard CRUD. Go custom when you need: - **Complex queries**: Joins, aggregations, CTEs — use `model.table` escape hatch with raw Drizzle - **Multi-model operations**: Actions that span multiple models or have complex business logic - **Non-CRUD actions**: Search, bulk operations, file processing — write a custom handler - **Custom hooks**: Before/after hooks that modify data or enforce business rules — use the [hooks system](/guide/basics/actions) ## vs Manual Model Files `createModel` replaces the manual model pattern documented in the [Database Overview](./index). Use it for standard CRUD. For custom queries, complex joins, or non-standard patterns, write manual model functions using `safeTry` + `handleError` directly and use the `table` escape hatch. --- url: /nile/guide/internals/logging.md --- # Logging **Type:** Reference / Specification **Path:** `logging/` ## 1. Purpose The logging module provides structured, append-only log persistence for Nile applications. It writes NDJSON log entries to disk via pino, supports optional time-based file chunking, and exposes a query API for reading logs back with filters. ### 1.1 Responsibilities - **Log creation** — Write structured NDJSON records to disk (production/test) or stdout (dev/agentic mode) - **File chunking** — Optionally split log files into time-based chunks (monthly, daily, weekly) organized in per-app directories - **Log retrieval** — Query logs with filters (appName, log_id, level, date range) with smart chunk selection to avoid scanning irrelevant files - **Factory API** — `createLogger(appName, config?)` returns a bound logger with `.info()`, `.warn()`, `.error()` methods ### 1.2 Non-Goals - **Size-based rotation** — The module does not implement log rotation by file size. Only time-based chunking is supported. - **Log shipping** — No built-in support for sending logs to external services (e.g., Datadog, Elasticsearch). Consumers can build this on top of the query API. - **Diagnostics logging** — Internal nile diagnostics (engine, REST, server boot messages) use `createDiagnosticsLog` from `utils/diagnostics-log.ts`, not this module. See section 7. ## 2. Architecture | File | Responsibility | |------|----------------| | `logger.ts` | Core logic: `createLog`, `getLogs`, `resolveLogPath`, `formatChunkName`, chunk helpers, types | | `create-log.ts` | `createLogger` factory — returns a bound logger with level methods | | `index.ts` | Barrel exports for the public API | ## 3. Public API ### 3.1 `createLogger(appName, config?)` **Path:** `logging/create-log.ts` Factory that returns a logger bound to a specific app name. Optionally accepts chunking config. ```typescript import { createLogger } from "@/logging"; // Flat mode (backwards compatible) — writes to logs/my-app.log const logger = createLogger("my-app"); // With monthly chunking — writes to logs/my-app/2026-02.log const logger = createLogger("my-app", { chunking: "monthly" }); logger.info({ atFunction: "handleRequest", message: "Request received", data: { path: "/api" } }); logger.warn({ atFunction: "validateInput", message: "Missing field" }); logger.error({ atFunction: "processOrder", message: "Payment failed", data: { orderId: "123" } }); ``` **Returns:** `{ info, warn, error }` — each method takes a `LogInput` (same as `Log` minus `appName`). ### 3.2 `createLog(log, config?)` **Path:** `logging/logger.ts` Lower-level function that writes a single log entry. Used internally by `createLogger`. ```typescript import { createLog } from "@/logging"; const logId = createLog({ appName: "my-app", atFunction: "startup", message: "Server started", level: "info", data: { port: 8000 }, }, { chunking: "daily" }); ``` **Behavior by MODE:** - `prod` / `NODE_ENV=test` — Writes NDJSON to the resolved log file path. Test mode uses `appendFileSync` for deterministic reads; prod uses pino async transport. - `agentic` — Returns the log record as a JSON string (no file I/O). - Any other value — Prints to `console.log` and returns `"dev-mode, see your dev console!"`. **Returns:** The generated `log_id` (nanoid, 6 chars), or JSON string in agentic mode. **Throws:** If `log.appName` is missing. ### 3.3 `getLogs(filters?, config?)` **Path:** `logging/logger.ts` Reads and filters log entries from disk. Supports both flat files and chunked directories. ```typescript import { getLogs } from "@/logging"; // All logs for an app (flat mode) const logs = getLogs({ appName: "my-app" }); // Filtered by level and date range (chunked mode) const errors = getLogs( { appName: "my-app", level: "error", from: new Date("2026-01-01"), to: new Date("2026-01-31") }, { chunking: "monthly" } ); ``` **Filters (`LogFilter`):** - `appName` — Filter by app name (required for chunked mode to locate the directory) - `log_id` — Filter by specific log ID - `level` — Filter by `"info"`, `"warn"`, or `"error"` - `from` / `to` — Date range filter (inclusive) **Smart chunk selection:** When chunking is enabled and date filters are provided, `getLogs` computes the date range of each chunk file and skips files that fall entirely outside the requested range. This avoids reading and parsing irrelevant files. **Returns:** `Log[]` — array of matching log entries. ### 3.4 `resolveLogPath(appName, config?)` Computes the file path for a given app and chunking config. Exported for testing and advanced use. ```typescript import { resolveLogPath } from "@/logging"; resolveLogPath("my-app"); // logs/my-app.log resolveLogPath("my-app", { chunking: "monthly" }); // logs/my-app/2026-02.log resolveLogPath("my-app", { chunking: "daily" }); // logs/my-app/2026-02-27.log resolveLogPath("my-app", { chunking: "weekly" }); // logs/my-app/2026-W09.log ``` Creates the app subdirectory if it doesn't exist. ### 3.5 `formatChunkName(date, chunking)` Formats a date into the chunk filename (without extension). Exported for testing and reuse. ```typescript import { formatChunkName } from "@/logging"; formatChunkName(new Date("2026-02-15"), "monthly"); // "2026-02" formatChunkName(new Date("2026-02-15"), "daily"); // "2026-02-15" formatChunkName(new Date("2026-02-15"), "weekly"); // "2026-W07" ``` ## 4. Key Types ### 4.1 `Log` ```typescript { atFunction: string; appName: string; message: string; data?: unknown; level?: "info" | "warn" | "error"; log_id?: string; } ``` The `level` field is used both in the TypeScript interface and in the serialized NDJSON records. Previously this was `type` in the interface and `level` in the JSON — this mismatch has been normalized. ### 4.2 `LoggerConfig` ```typescript { chunking?: "monthly" | "daily" | "weekly" | "none"; } ``` - `"none"` (default) — Single flat file per app: `logs/{appName}.log` - `"monthly"` — `logs/{appName}/YYYY-MM.log` - `"daily"` — `logs/{appName}/YYYY-MM-DD.log` - `"weekly"` — `logs/{appName}/YYYY-WNN.log` (ISO 8601 week number) ### 4.3 `LogFilter` ```typescript { appName?: string; log_id?: string; level?: "info" | "warn" | "error"; from?: Date; to?: Date; } ``` ## 5. File Layout ### 5.1 Flat Mode (default) ``` logs/ my-app.log # NDJSON, one record per line another-app.log ``` ### 5.2 Chunked Mode ``` logs/ my-app/ 2026-01.log # monthly 2026-02.log daily-app/ 2026-02-25.log # daily 2026-02-26.log 2026-02-27.log weekly-app/ 2026-W08.log # weekly (ISO week) 2026-W09.log ``` Each file contains NDJSON records identical in format to flat mode. The only difference is where they are written. ## 6. Internal Helpers These functions are not exported but are critical to `getLogs` performance: - `resolveLogFiles(filters, chunking)` — Determines which files to read. For flat mode, returns the single file. For chunked mode, scans the app directory and filters by date relevance. - `isChunkRelevant(filename, chunking, filters)` — Checks if a chunk file's date range overlaps with the filter's `from`/`to` range. - `getChunkDateRange(chunkName, chunking)` — Parses a chunk filename into start/end date boundaries. - `readAndParseLogFiles(files)` — Reads NDJSON from multiple files into a single array, skipping malformed lines. - `applyLogFilters(logs, filters)` — Applies `appName`, `log_id`, `level`, and time range filters. - `getISOWeekNumber(date)` — ISO 8601 week number calculation. - `getDateFromISOWeek(year, week)` — Returns the Monday of a given ISO week. ## 7. Diagnostics Logging (Nile Internals) Nile's internal modules (server, engine, REST) use a separate diagnostics logging system that is distinct from this module. The `createDiagnosticsLog` utility in `utils/diagnostics-log.ts` provides centralized diagnostic output: ```typescript import { createDiagnosticsLog } from "@/utils"; const log = createDiagnosticsLog("Engine", { diagnostics: config.diagnostics, logger: nileContext.resources?.logger, }); log("Initialized in 2ms. Loaded 3 services."); ``` **Behavior:** - When `diagnostics` is `false` (or absent), returns a no-op function - When `diagnostics` is `true`, checks `resources.logger` first, falls back to `console.log` - The prefix (e.g. `"Engine"`) is automatically prepended as `[Engine]` This replaces the previous pattern where `server.ts`, `rest.ts`, and `engine.ts` each defined their own inline `log()` closures. ## 8. `handleError` — Userland Error Utility **Path:** `utils/handle-error.ts` A utility for application developers that combines error logging and error return in a single call. Infers `atFunction` from the caller stack when not provided. ```typescript import { handleError } from "@nilejs/nile"; // With explicit logger return handleError({ message: "Invalid credentials", data: { phone_number: data.phone_number }, logger: log, }); // Without explicit logger — uses getContext().resources.logger return handleError({ message: "User not found", data: { userId: data.id }, }); ``` ### 8.1 Behavior 1. **Logger resolution:** Uses explicit `logger` param if provided, otherwise calls `getContext()` and uses `ctx.resources.logger`. If neither is available, throws. 2. **atFunction inference:** Parses `new Error().stack` to extract the caller function name. Falls back to `"unknown"` if parsing fails. Override via the `atFunction` param. 3. **Logging:** Calls `logger.error({ atFunction, message, data })` — receives a `log_id` back 4. **Return:** Returns `Err("[log_id] message")` — error ID first, then user-facing message ### 8.2 Interface ```typescript interface HandleErrorParams { message: string; // User-facing error message data?: unknown; // Optional context data to log logger?: NileLogger; // Explicit logger instance atFunction?: string; // Override auto-detected caller name } ``` ### 8.3 Constraints - **Logger required** — Throws if no explicit logger and no `resources.logger` on the context - **Stack parsing** — Relies on `Error().stack` which may behave differently across runtimes. Override `atFunction` when the inferred name is unhelpful (e.g., arrow functions, callbacks) ### 8.4 Failure Modes - **No logger available** — Throws `"handleError: No logger available. Provide a logger param or set resources.logger on server config."` ## 8. Constraints - **MODE required** — `createLog` throws if `process.env.MODE` is not set (lazy-evaluated on first log call, not at import time) - **appName required** — `createLog` throws if `log.appName` is missing - **Chunked getLogs requires appName** — Without `appName`, chunked mode returns an empty array (no directory to scan) - **No concurrent write safety** — In test mode, uses `appendFileSync`. In production, pino handles buffering. No file-level locking is implemented. - **Pino transport per call** — In production, `createLog` creates a new pino transport for each log entry. Callers writing many logs should use `createLogger` and consider caching. ## 9. Failure Modes - **Missing MODE** — Throws `"Missing MODE environment variable"` on first `createLog` call - **Missing appName** — Throws immediately with the stringified log object for debugging - **Malformed log lines** — `getLogs` silently skips lines that fail `JSON.parse` (NDJSON tolerance) - **Missing log directory** — Created automatically on first write (`mkdirSync` with `{ recursive: true }`) - **Unparseable chunk filenames** — `isChunkRelevant` returns `true` (includes the file to be safe rather than silently dropping data) --- url: /nile/guide/internals/engine.md --- # Service Action Engine **Type:** Reference / Specification **Path:** `engine/` ## 1. Purpose The Service Action Engine provides a high-performance, $O(1)$ routing and introspection layer for business operations, and a unified execution pipeline. It maps a flat array of domain-specific `Services` (and their nested `Actions`) into pre-computed memory structures upon initialization. This engine is designed to sit below the HTTP/RPC transport layer (e.g., Hono), decoupling the knowledge of available actions from the transport mechanism used to invoke them. ### 1.1 Responsibilities * **Initialization:** Parse the `services` array exactly once on boot. * **Introspection:** Provide lightweight, zero-latency metadata for available services and actions to enable dynamic discovery. * **Routing:** Provide strict $O(1)$ memory pointer lookups for specific actions based on `serviceName` and `actionName`. * **Execution Pipeline:** Process the full action lifecycle safely, including Global/Action-level Hooks, Payload Validation (Zod), and safe Handler execution. * **Diagnostics:** Emit timing and status information via `createDiagnosticsLog` from `utils/diagnostics-log.ts` when `diagnostics` is enabled. See `docs/internals/logging.md` section 7. * **Result Pattern Enforcement:** Ensure all internal engine methods return a `Result` from the `slang-ts` library to eliminate `try/catch` requirements in the transport layer. ### 1.2 Non-Goals * **HTTP Routing:** The engine has no concept of HTTP methods, headers, or paths. ## 2. Architecture and Data Structures To achieve guaranteed $O(1)$ lookups and prevent latency spikes, the engine pre-computes three internal data structures during `createEngine`: 1. `serviceSummaries`: An array of `ServiceSummary` objects used for fast enumeration of all available services. 2. `serviceActionsStore`: A dictionary mapping a `serviceName` to an array of lightweight `ActionSummary` objects. This avoids sending bulky schema/handler definitions during introspection. 3. `actionStore`: A nested dictionary (`Record>`) that holds the exact memory pointers to the full `Action` objects for execution routing. The engine execution pipeline helpers are extracted into `pipeline.ts` to keep the code modular and under the 400 LOC limit. ## 3. Public API The engine exposes four strictly-typed methods. All methods return a `slang-ts` `Result`. ### 3.1 `getServices()` Returns an array of all registered services. **Returns:** `Result` ```typescript const result = engine.getServices(); if (result.isOk) { const services = result.value; // [ { name: 'auth', description: '...', actions: ['login', 'logout'] } ] } ``` ### 3.2 `getServiceActions(serviceName: string)` Returns lightweight metadata for all actions within a specific service. **Returns:** `Result` ```typescript const result = engine.getServiceActions('auth'); if (result.isOk) { const actions = result.value; // [ { name: 'login', isProtected: false, validation: true, accessControl: ['public'] } ] } ``` *Note:* The `validation` property is a boolean indicating whether the action has a defined Zod schema (`!!action.validation`). ### 3.3 `getAction(serviceName: string, actionName: string)` Returns the full, executable `Action` object. Typically used internally by the execution pipeline. **Returns:** `Result` ```typescript const result = engine.getAction('auth', 'login'); if (result.isOk) { const action = result.value; // { name: 'login', handler: [Function], validation: ZodObject, hooks: {...} } } ``` ### 3.4 `executeAction(serviceName: string, actionName: string, payload: unknown, nileContext: NileContext)` Executes an action through the full pipeline (Global Before Hook -> Action Before Hooks -> Validation -> Handler -> Action After Hooks -> Global After Hook). The caller must provide a `NileContext` instance — the engine never creates one internally. **Returns:** `Promise>` ```typescript const nileContext = createNileContext(); const result = await engine.executeAction('auth', 'login', { username: 'test', password: '123' }, nileContext); if (result.isOk) { const data = result.value; } else { console.error("Action failed:", result.error); } ``` ## 4. Execution Pipeline When `executeAction` is called, the following steps run in sequence: 1. **Global Before Hook** (`onBeforeActionHandler`) — Pass/fail guard only, does not mutate payload 2. **Action-Level Before Hooks** (`action.hooks.before`) — Sequential, output becomes next input (mutates payload) 3. **Zod Validation** — Uses `action.validation.safeParse()` with `prettifyError` for formatting 4. **Main Handler** — Core business logic 5. **Action-Level After Hooks** (`action.hooks.after`) — Sequential, mutates result 6. **Global After Hook** (`onAfterActionHandler`) — Final cleanup/logging ### 4.1 Hook Failure Behavior - Each `HookDefinition` has an `isCritical: boolean` flag - `isCritical: true` — if the hook returns `Err` or throws, the pipeline halts immediately - `isCritical: false` — failure is logged but execution continues with the previous value ### 4.2 Pipeline Response Mode If an action sets `result: { pipeline: true }`, the return includes the full hook execution log: ```typescript // Standard return Ok(data) // Pipeline mode return Ok({ data: data, pipeline: { before: [ { name: "service.hook", passed: true, input: ..., output: ... } ], after: [] } }) ``` ## 5. Crash Safety (`safeTry`) All handler and hook invocations in `pipeline.ts` are wrapped in `safeTry` from `slang-ts`. This prevents uncaught exceptions from crashing the process. Protected call sites: - `runHook` — action-level before/after hook handlers - `runHandler` — main action handler - `runGlobalBeforeHook` — global before hook - `runGlobalAfterHook` — global after hook If a handler throws instead of returning a `Result`, `safeTry` catches the exception and returns `Err(error.message)`. The pipeline then handles it identically to a handler-returned `Err`. ## 6. Constraints and Failure Modes ### 6.1 Constraints * **Memory Bound:** Because the engine loads all `services` and their dependencies (Zod schemas, DB models via imports) into memory upfront, it is designed for persistent, long-running server environments (e.g., standard Node.js/Bun containers), not aggressive cold-start environments. * **Immutability:** The initialized stores (`actionStore`, etc.) are closed over in the factory function and cannot be modified at runtime. Dynamic injection of actions post-boot is not supported. * **File Size:** The core `engine.ts` must remain under 400 LOC, relying on `pipeline.ts` for pipeline steps. ### 6.2 Failure Modes * **Missing Service/Action:** Calling `getServiceActions`, `getAction`, or `executeAction` with an unregistered name will immediately return an `Err(string)` result. The transport layer must handle this by returning a `404 Not Found` or equivalent error to the client. * **Duplicate Detection:** The engine throws immediately on boot if it encounters duplicate service names or duplicate action names within the same service. This prevents silent overwrites and catches configuration errors early. - Duplicate service name → `Error: Duplicate service name ''. Service names must be unique.` - Duplicate action name → `Error: Duplicate action name '' in service ''. Action names must be unique within a service.` - The same action name in *different* services is valid and does not throw. ## 7. Key Types All types below are exported from `index.ts` and defined in `engine/types.ts`. ### 7.1 `EngineOptions` Configuration passed to `createEngine`: ```typescript { diagnostics?: boolean; services: Services; onBeforeActionHandler?: BeforeActionHandler; onAfterActionHandler?: AfterActionHandler; } ``` `createEngine` is consumed internally by `createNileServer` — developers configure these values via `ServerConfig`. ### 7.2 `HookContext` Tracks the full lifecycle state of a single action execution. Attached to `NileContext.hookContext` and reset at the start of each `executeAction` call. ```typescript { actionName: string; input: unknown; output?: unknown; error?: string; state: Record; log: { before: HookLogEntry[]; after: HookLogEntry[]; }; } ``` - `state` — mutable key-value store for hooks to share data within a single execution - `log` — accumulated `HookLogEntry` records from before/after hook phases ### 7.3 `HookLogEntry` A single hook execution record: ```typescript { name: string; // "serviceName.actionName" input: unknown; output: unknown; passed: boolean; } ``` ### 7.4 `HookDefinition` Declares a hook as a reference to another action in the system: ```typescript { service: string; action: string; isCritical: boolean; } ``` See section 4.1 for `isCritical` behavior. ### 7.5 `ActionResultConfig` Controls the shape of `executeAction` return values: ```typescript { pipeline: boolean; } ``` When `pipeline: true`, the result includes the full hook execution log alongside the data. See section 4.2. ## 8. Factory Functions The `@nilejs/nile` package exports typed identity functions for defining services and actions with full type inference. ### 8.1 `createAction` Creates a single action with full type inference. No runtime overhead — returns the config as-is. ```typescript import { createAction } from '@nilejs/nile'; export const loginAction = createAction({ name: 'login', description: 'User login', handler: async (data, ctx) => { /* ... */ }, validation: loginSchema, isProtected: false, accessControl: ['public'], }); ``` ### 8.2 `createActions` Creates multiple actions at once. This is optional — you can also pass action arrays directly. ```typescript import { createActions } from '@nilejs/nile'; export const authActions = createActions([ createAction({ name: 'login', description: '...', handler: loginHandler, validation: loginSchema }), createAction({ name: 'logout', description: '...', handler: logoutHandler }), ]); ``` ### 8.3 `createService` Creates a service with full type inference. ```typescript import { createService } from '@nilejs/nile'; export const authService = createService({ name: 'auth', description: 'Authentication service', actions: authActions, }); ``` ### 8.4 `createServices` Creates multiple services at once. ```typescript import { createServices } from '@nilejs/nile'; export const allServices = createServices([ authService, userService, taskService, ]); ``` ### 8.5 Recommended Project Structure For larger applications, organize actions one-per-file in domain folders. Define all services in a single `services.config.ts` file that imports the actions and exports the services array. No barrel (`index.ts`) file per service folder is needed. Keep database code in a separate `db/` directory — schema definitions, client setup, and model files that encapsulate all data access logic. See [Database Utilities](/guide/internals/db) for the full model file pattern. ``` src/ ├── db/ │ ├── client.ts # database client setup (e.g. PGlite + Drizzle) │ ├── schema.ts # Drizzle table definitions │ ├── types.ts # inferred types from schema │ ├── index.ts # barrel exports │ └── models/ │ ├── tasks.ts # CRUD model functions for tasks table │ ├── users.ts # CRUD model functions for users table │ └── index.ts # barrel exports ├── services/ │ ├── auth/ │ │ ├── login.ts # exports loginAction │ │ ├── logout.ts # exports logoutAction │ │ └── profile.ts # exports profileAction │ ├── tasks/ │ │ ├── create.ts # exports createTaskAction │ │ ├── list.ts # exports listTaskAction │ │ ├── get.ts # exports getTaskAction │ │ ├── update.ts # exports updateTaskAction │ │ └── delete.ts # exports deleteTaskAction │ └── services.config.ts # imports all actions, defines all services, exports Services array ├── server.config.ts # imports services, exports ServerConfig (optional, can be inline in index.ts) └── index.ts # imports server config/services, creates server ``` Action handlers call model functions for data access — they should not contain raw database queries. Models handle validation, error logging, and return `Result` types that handlers forward to the client. Each action file defines the handler inline (not exported) and only exports the action: ```typescript // services/auth/login.ts import { Ok } from 'slang-ts'; import z from 'zod'; import { createAction } from '@nilejs/nile'; const loginSchema = z.object({ username: z.string(), password: z.string(), }); const loginHandler = (data) => { // ... validation and logic return Ok({ userId: '123' }); }; export const loginAction = createAction({ name: 'login', description: 'User login', handler: loginHandler, validation: loginSchema, }); ``` The `services.config.ts` file imports all actions and defines services using `createServices`: ```typescript // services/services.config.ts import { createServices, type Services } from '@nilejs/nile'; import { loginAction } from './auth/login'; import { logoutAction } from './auth/logout'; import { profileAction } from './auth/profile'; import { createTaskAction } from './tasks/create'; import { listTaskAction } from './tasks/list'; export const services: Services = createServices([ { name: 'auth', description: 'Authentication service', actions: [ loginAction, logoutAction, profileAction, ], }, { name: 'tasks', description: 'Task management service', actions: [ createTaskAction, listTaskAction, ], }, ]); ``` For larger applications, you may extract the server configuration into a separate `server.config.ts` that imports the services array. For smaller projects, defining the config directly in `index.ts` is equally valid. ### 8.6 Alternative — Barrel File Pattern An alternative (not recommended for most projects) is to create a barrel file per service folder using `createService`. This adds a file per domain but can be useful for very large codebases where you want explicit service boundaries: ``` services/ ├── auth/ │ ├── login.ts # exports loginAction │ ├── logout.ts # exports logoutAction │ └── index.ts # imports actions, exports authService via createService ├── tasks/ │ ├── create.ts │ ├── list.ts │ └── index.ts # exports taskService via createService └── index.ts # imports all services, exports via createServices ``` ```typescript // services/auth/index.ts import { createAction, createService } from '@nilejs/nile'; import { loginAction } from './login'; import { logoutAction } from './logout'; export const authService = createService({ name: 'auth', description: 'Authentication service', actions: [loginAction, logoutAction], }); ``` --- url: /nile/guide/internals/rest.md --- # REST Interface **Type:** Reference / Specification **Path:** `rest/` ## 1. Purpose The REST interface exposes the Action Engine over HTTP via Hono. It implements a single-POST-endpoint architecture where all service communication flows through one route, discriminated by an `intent` field in the request body. ### 1.1 Responsibilities - **Request validation** — Validates incoming JSON against a Zod schema before processing - **Intent routing** — Dispatches `explore`, `execute`, and `schema` intents to dedicated handlers - **Response mapping** — Converts internal `Result` types to the `ExternalResponse` shape at the HTTP boundary - **Middleware application** — CORS, rate limiting, and static file serving - **Diagnostics** — Emit request routing information via `createDiagnosticsLog` from `utils/diagnostics-log.ts` when `diagnostics` is enabled. See `docs/internals/logging.md` section 7. ### 1.2 Non-Goals - **Business logic** — The REST layer does not contain domain logic. It delegates to the engine. ## 2. Architecture The REST module is split into three files to stay under the 400 LOC limit: | File | LOC | Responsibility | |------|-----|----------------| | `rest.ts` | 136 | Hono app factory, request validation, route registration | | `intent-handlers.ts` | 236 | Explore, execute, schema handlers, `toExternalResponse`, `intentHandlers` lookup | | `middleware.ts` | 108 | `applyRateLimiting`, `applyStaticServing` | ## 3. Endpoints ### 3.1 `POST {baseUrl}/services` The single endpoint for all service interactions. The request body must conform to `ExternalRequest`: ```typescript { intent: "explore" | "execute" | "schema"; service: string; // service name or "*" for wildcard action: string; // action name or "*" for wildcard payload: Record; } ``` The body is validated against a Zod schema. Invalid JSON or missing fields return `400`. ### 3.2 `GET /status` Health check endpoint. Only registered when `config.enableStatus` is `true`. Returns: ```json { "status": true, "message": "{serverName} is running", "data": {} } ``` ### 3.3 404 Handler All unmatched routes return: ```json { "status": false, "message": "Route not found. Use POST {baseUrl}/services for all operations.", "data": {} } ``` ## 4. Intent Handlers Intent dispatch uses an object lookup (`intentHandlers`) rather than switch/if-else. ### 4.1 Explore Discovers services and actions. | `service` | `action` | Behavior | |-----------|----------|----------| | `"*"` | any | List all services via `engine.getServices()` | | `"name"` | `"*"` | List actions for service via `engine.getServiceActions()` | | `"name"` | `"name"` | Return action metadata (name, description, isProtected, accessControl, hooks, meta) | ### 4.2 Execute Runs an action through the engine pipeline. Wildcards are rejected — both `service` and `action` must be specific. Calls `engine.executeAction(service, action, payload, nileContext)` and maps the result. ### 4.3 Schema Exports Zod validation schemas as JSON Schema (via `z.toJSONSchema()` from Zod v4). | `service` | `action` | Behavior | |-----------|----------|----------| | `"*"` | any | All schemas across all services | | `"name"` | `"*"` | All schemas in a service | | `"name"` | `"name"` | Single action schema | Actions without a `validation` schema return `null`. Schema conversion failures are caught by `safeTrySync` and also return `null`. ## 5. Response Format All responses use the `ExternalResponse` shape: ```typescript { status: boolean; message: string; data: { error_id?: string; [key: string]: unknown; }; } ``` The `toExternalResponse` function handles the `Result` to `ExternalResponse` mapping: - `Ok(value)` — if value is a plain object, it becomes `data` directly. Arrays and primitives are wrapped as `{ result: value }`. - `Err(message)` — `status: false`, message is the error string, `data` is empty. HTTP status codes: `200` for success, `400` for failures and validation errors, `404` for unmatched routes. ## 6. Middleware ### 6.1 CORS Applied first via `applyCorsConfig` from `cors/cors.ts`. See `docs/internals/cors.md`. ### 6.2 Rate Limiting **File:** `rest/middleware.ts` — `applyRateLimiting` Only applied when `config.rateLimiting.limitingHeader` is set. Uses `hono-rate-limiter`. - Client key is extracted from the configured request header - If the header is missing, falls back to a shared `__unknown_client__` key (graceful degradation, not a crash) - Defaults: 100 requests per 15-minute window ### 6.3 Static File Serving **File:** `rest/middleware.ts` — `applyStaticServing` Only applied when `config.enableStatic` is `true`. Supports both `"bun"` and `"node"` runtimes. - Serves files from `./assets` at `/assets/*` - Dynamically imports the runtime-specific adapter (`hono/bun` for Bun, `@hono/node-server/serve-static` for Node) - The import result is cached after first successful load - Import failures are caught by `safeTry` — static serving is silently skipped ## 7. Key Types ### 7.1 `RestConfig` ```typescript { baseUrl: string; host?: string; port?: number; diagnostics?: boolean; enableStatic?: boolean; enableStatus?: boolean; rateLimiting?: RateLimitConfig; allowedOrigins: string[]; cors?: CorsConfig; uploads?: { /* not yet implemented */ }; } ``` ### 7.2 `RateLimitConfig` ```typescript { windowMs?: number; // default: 900000 (15 min) limit?: number; // default: 100 standardHeaders?: boolean; // default: true limitingHeader: string; // required — header name for client key store?: Store; // custom rate limiter store diagnostics?: boolean; } ``` ### 7.3 `ExternalRequest` ```typescript { intent: "explore" | "execute" | "schema"; service: string; action: string; payload: Record; } ``` ### 7.4 `ExternalResponse` ```typescript { status: boolean; message: string; data: { error_id?: string; [key: string]: unknown }; } ``` ## 8. Constraints - **Single POST endpoint** — All service interactions go through `POST {baseUrl}/services`. No per-action routes. - **No streaming** — Responses are JSON only. No SSE or chunked transfer. - **Rate limiter requires header** — Without `limitingHeader`, rate limiting is not applied at all. ## 9. Failure Modes - **Invalid JSON body** — Returns `400` with "Invalid or missing JSON body" - **Schema validation failure** — Returns `400` with Zod issue details in `data.errors` - **Wildcard in execute** — Returns `400` with descriptive message - **Missing service/action** — Engine returns `Err`, mapped to `400` via `toExternalResponse` - **Handler crash** — Caught by `safeTry` in the engine pipeline, returned as `Err` --- url: /nile/guide/internals/cors.md --- # CORS Middleware **Type:** Reference / Specification **Path:** `cors/` ## 1. Purpose The CORS module configures Cross-Origin Resource Sharing middleware on the Hono app. It supports global defaults derived from `RestConfig.allowedOrigins`, per-route static overrides, and dynamic resolver functions for runtime origin decisions. ### 1.1 Responsibilities - **Default CORS derivation** — Build sensible defaults from `RestConfig.allowedOrigins` - **Global middleware** — Apply a catch-all CORS handler to all routes - **Route-specific rules** — Apply per-path overrides or resolver-based CORS before the global handler - **Security boundary** — Deny access (empty origin) when resolvers throw errors ### 1.2 Non-Goals - **Authentication** — CORS is a browser security mechanism, not an auth layer - **Request blocking** — CORS headers influence browser behavior but do not block server-side requests ## 2. Architecture | File | LOC | Responsibility | |------|-----|----------------| | `cors.ts` | 140 | `buildDefaultCorsOptions`, `applyCorsConfig`, route rule application, resolver evaluation | | `types.ts` | 94 | `CorsOptions`, `CorsResolver`, `CorsRouteRule`, `CorsConfig` | `applyCorsConfig` accepts `RestConfig` directly (not `ServerConfig`). ## 3. Configuration Flow ### 3.1 Enabled States `CorsConfig.enabled` controls whether CORS middleware is applied: - `true` or `"default"` (default) — CORS middleware is active - `false` — No CORS middleware is applied, no CORS headers are set ### 3.2 Default Options `buildDefaultCorsOptions` derives defaults from `RestConfig`: ```typescript { origin: config.cors?.defaults?.origin ?? getDefaultOrigin, credentials: true, allowHeaders: ["Content-Type", "Authorization"], allowMethods: ["POST", "GET", "OPTIONS"], exposeHeaders: ["Content-Length"], maxAge: 600 } ``` Origin resolution when no `cors.defaults.origin` is specified: - If `allowedOrigins` has entries — request origin is checked against the list, rejected origins get `""` - If `allowedOrigins` is empty — returns `"*"` (allow all) All defaults can be overridden via `cors.defaults`. ### 3.3 Application Order 1. **Route-specific rules** (`cors.addCors[]`) are applied first as path-scoped middleware 2. **Global CORS** (`app.use("*", cors(...))`) is applied last as a catch-all This order matters in Hono: route-specific middleware runs before global middleware for matching paths. ## 4. Route Rules (`CorsRouteRule`) Each rule targets a specific path and can use either static options or a dynamic resolver: ```typescript { path: "/api/public/*", options: { origin: "*", credentials: false } } ``` Or with a resolver: ```typescript { path: "/api/partners/*", resolver: (origin, c) => { if (partnerOrigins.includes(origin)) return true; return false; } } ``` If both `options` and `resolver` are present, `resolver` takes precedence. ## 5. Resolver Behavior A `CorsResolver` function receives the request origin and Hono context, and returns: | Return value | Behavior | |-------------|----------| | `true` | Allow origin with default options | | `false` | Deny (origin set to `""`) | | `CorsOptions` object | Merge with defaults and use | | `undefined` | Use default options | | **throws** | **Deny** (origin set to `""`) | The deny-on-error behavior is a security decision: resolver failures never fall through to allow. ## 6. Key Types ### 6.1 `CorsConfig` ```typescript { enabled?: boolean | "default"; defaults?: CorsOptions; addCors?: CorsRouteRule[]; } ``` ### 6.2 `CorsOptions` ```typescript { origin?: string | string[] | ((origin: string, c: Context) => string | undefined | null); allowMethods?: string[] | ((origin: string, c: Context) => string[]); allowHeaders?: string[]; maxAge?: number; credentials?: boolean; exposeHeaders?: string[]; } ``` Compatible with Hono's `cors()` middleware parameter shape. ### 6.3 `CorsResolver` ```typescript type CorsResolver = (origin: string, c: Context) => boolean | CorsOptions | undefined; ``` ### 6.4 `CorsRouteRule` ```typescript { path: string; options?: CorsOptions; resolver?: CorsResolver; } ``` ## 7. Constraints - **Hono middleware ordering** — The global `app.use("*", cors(...))` also fires on routes that have route-specific rules. In practice this means the global handler may overwrite route-specific headers. This is known Hono behavior. - **No preflight caching per-route** — `maxAge` is set globally. Route-specific `maxAge` overrides are applied but browser caching behavior may vary. ## 8. Failure Modes - **Resolver throws** — Caught, logged to `console.error`, origin set to `""` (deny) - **Empty `allowedOrigins` with no `cors.defaults.origin`** — Falls through to `"*"` (allow all). This is intentional for development convenience but should be restricted in production. - **`enabled: false`** — No CORS middleware is applied. Browsers will block cross-origin requests entirely. --- url: /nile/guide/others/faq.md --- # FAQ Common questions about building with Nile, with accurate answers that reflect the current state of the framework. --- ## How do I ensure consistent error responses across my API? Nile has a layered error handling strategy that guarantees every response follows the same shape — no matter what goes wrong. ### Result Pattern at Function Boundaries Every action handler returns `Ok(data)` for success or `Err("message")` for failure using the `slang-ts` Result type. This is enforced by the type system — handlers cannot return raw values or throw to signal errors. ```typescript import { Ok, Err } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; export const getUser: Action = createAction({ name: "getUser", description: "Get user by ID", handler: (data) => { if (!data.id) return Err("User ID is required"); const user = findUser(data.id as string); if (!user) return Err("User not found"); return Ok({ user }); }, }); ``` Both `Ok` and `Err` produce the same consistent response shape at the HTTP boundary: ```json // Ok path { "status": true, "message": "Action 'users.getUser' executed", "data": { "user": { ... } } } // Err path { "status": false, "message": "User not found", "data": {} } ``` ### `handleError` for Runtime Errors For errors that should be logged and traced (database failures, unexpected state, external service errors), use `handleError`. It logs the error via the configured logger and returns an `Err` with a traceable log ID: ```typescript import { Ok } from "slang-ts"; import { createAction, handleError, type Action } from "@nilejs/nile"; export const createOrder: Action = createAction({ name: "createOrder", description: "Create a new order", handler: async (data, context) => { const db = context?.resources?.database; if (!db) { return handleError({ message: "Database not available" }); // Returns: Err("[log-id-xyz] Database not available") } const result = await saveOrder(db, data); if (!result) { return handleError({ message: "Failed to save order", data: { payload: data }, atFunction: "createOrder", }); } return Ok({ order: result }); }, }); ``` `handleError` resolves the logger from context automatically. The returned error string includes the log ID, making it easy to trace issues in production logs. ### Crash Safety Even if a handler throws an unhandled exception, the engine's `safeTry` wrapper catches it and converts it to `Err(error.message)`. The response shape stays consistent — the client never receives a raw stack trace or 500 error. ### Client Side The `@nilejs/client` maps server responses to `{ error, data }`. Network failures, timeouts, and server errors all land in the `error` field: ```typescript const { error, data } = await nile.invoke({ service: "users", action: "getUser", payload: { id: "123" }, }); if (error) { // error = "User not found" or "[log-id] Database not available" showErrorToast(error); } else { renderUser(data); } ``` **Summary:** Use `Err("message")` for expected business errors. Use `handleError(...)` for runtime errors that need logging. The framework handles the rest — consistent shapes all the way from handler to client. --- ## How do I handle authentication and authorization across multiple services? Nile has built-in JWT authentication and a hook-based authorization model. ### Server-Level Auth Config Configure JWT verification once at the server level. Every service and action in your application shares this config: ```typescript import { createNileServer } from "@nilejs/nile"; const server = createNileServer({ name: "MyApp", services: [/* ... */], auth: { secret: process.env.JWT_SECRET!, method: "header", // or "cookie" }, }); ``` ### Per-Action Protection Mark individual actions as protected. The engine verifies the JWT before the handler runs — no auth code in your business logic: ```typescript import { Ok, Err } from "slang-ts"; import { createAction, type Action } from "@nilejs/nile"; // Public — no auth required export const listProducts: Action = createAction({ name: "listProducts", description: "List all products", handler: () => Ok({ products: [] }), }); // Protected — requires valid JWT export const createProduct: Action = createAction({ name: "createProduct", description: "Create a product", isProtected: true, handler: (data, context) => { const user = context?.getUser(); return Ok({ product: { ...data, createdBy: user?.userId } }); }, }); ``` This works the same across every service. A protected action in the `orders` service and a protected action in the `products` service both go through the same JWT verification step. ### Authorization via Hooks For role-based access control or custom authorization logic, use `onBeforeActionHandler`. This hook runs after JWT verification but before the action handler, giving you the verified user identity: ```typescript import { Ok, Err } from "slang-ts"; const server = createNileServer({ name: "MyApp", services: [/* ... */], auth: { secret: process.env.JWT_SECRET! }, onBeforeActionHandler: async (request, context) => { const user = context.getUser(); if (!user) return; // Unprotected action, let it through const requiredRole = request.action.accessControl?.[0]; if (requiredRole && user.role !== requiredRole) { return Err(`Requires role: ${requiredRole}`); } return Ok(request.payload); }, }); ``` Actions declare their required roles via `accessControl`: ```typescript export const deleteUser: Action = createAction({ name: "deleteUser", description: "Delete a user account", isProtected: true, accessControl: ["admin"], handler: (data, context) => { // Only reaches here if JWT is valid AND user has "admin" role return Ok({ deleted: true }); }, }); ``` See the full [Authentication guide](/guide/basics/auth) for token sources, JWT claims mapping, and cookie-based auth. --- ## How does Nile handle API routing? Nile does **not** generate REST endpoints from your service definitions. This is a fundamental architectural difference from frameworks like Express, Fastify, or NestJS. ### Single Endpoint, Intent-Based Routing All communication flows through one endpoint: ``` POST {baseUrl}/services ``` The request body tells Nile what to do: ```json { "intent": "execute", "service": "tasks", "action": "create", "payload": { "title": "Buy milk" } } ``` There is no `/tasks/create` route. There is no `GET /tasks/:id`. Every operation — whether it's creating a task, listing users, or checking auth — goes through the same endpoint with a different `intent`, `service`, and `action` combination. ### Why This Is Faster Traditional endpoint-based frameworks match incoming requests against a route table — often a trie or regex-based router. As your API grows, route matching scales with the number of endpoints. Nile uses pre-computed O(1) dictionary lookups. The engine builds a nested `Record>` at boot. Finding the right handler is a two-key object lookup regardless of how many services or actions exist. No route parsing, no regex matching, no middleware stacks per route. ### Three Intents | Intent | Purpose | Example | |--------|---------|---------| | `execute` | Run an action's business logic | Create a task, update a user | | `explore` | Discover available services and actions | List all services, get action metadata | | `schema` | Retrieve validation schemas as JSON Schema | Generate client types, build dynamic forms | ### Built-In Discovery Unlike endpoint-based APIs that need separate documentation (OpenAPI, Swagger), Nile's `explore` intent provides runtime discovery. Any client can query what services and actions are available, what fields they accept, and whether they require authentication — all through the same endpoint. ```typescript // Client-side discovery const { data: services } = await nile.explore({ service: "*", action: "*" }); // Returns all services with their actions, descriptions, and metadata ``` ### Embrace the Model If you're coming from endpoint-based frameworks, the mental shift is: - **Service** = domain grouping (replaces route prefixes like `/users`, `/tasks`) - **Action** = operation (replaces individual route handlers) - **Intent** = what you want to do (replaces HTTP verbs) This model scales cleanly. Adding a new operation means adding an action to a service — no route registration, no middleware configuration, no path conflicts. --- ## How do I handle complex, multi-step workflows? Nile handles workflow complexity through action composition, hook pipelines, and shared execution state. ### Actions Are Just Functions Any action can contain arbitrarily complex business logic. There's no artificial constraint on what a handler does: ```typescript export const processOrder: Action = createAction({ name: "processOrder", description: "Validate, charge, and fulfill an order", isProtected: true, handler: async (data, context) => { // Step 1: Validate inventory const inventory = await checkInventory(data.items); if (!inventory.available) return Err("Items out of stock"); // Step 2: Process payment const payment = await chargePayment(data.paymentMethod, data.total); if (!payment.success) { return handleError({ message: "Payment failed", data: { reason: payment.error } }); } // Step 3: Create fulfillment const fulfillment = await createFulfillment(data.items, data.shippingAddress); return Ok({ orderId: payment.transactionId, fulfillment }); }, }); ``` ### Hook Pipelines for Composable Steps When workflow steps are reusable across actions, define them as separate actions and wire them via hooks. Before hooks run sequentially — each hook's output becomes the next hook's input: ```typescript // A validation action reused across multiple services export const validateStock: Action = createAction({ name: "validateStock", description: "Check inventory availability", handler: async (data) => { const available = await checkStock(data.items); if (!available) return Err("Out of stock"); return Ok(data); // Pass through to next step }, }); // The main action with hooks export const createOrder: Action = createAction({ name: "createOrder", description: "Create an order", hooks: { before: [ { service: "inventory", action: "validateStock", isCritical: true }, { service: "pricing", action: "applyDiscounts", isCritical: true }, ], after: [ { service: "notifications", action: "sendConfirmation", isCritical: false }, ], }, handler: (data, context) => { // data has been validated and enriched by before hooks return Ok({ order: { ...data, status: "confirmed" } }); }, }); ``` The `isCritical` flag controls failure behavior: critical hooks halt the pipeline on error, non-critical hooks log the failure and continue. ### Shared State Within a Pipeline Hooks within a single execution share state via `hookContext.state`: ```typescript // In a before hook's action handler handler: (data, context) => { const discount = calculateDiscount(data); context?.hookContext?.state.discountApplied = discount; return Ok({ ...data, discount }); }, // In the main handler handler: (data, context) => { const discount = context?.hookContext?.state.discountApplied; // Use the discount calculated by the before hook return Ok({ total: data.subtotal - discount }); }, ``` ### Cross-Request State For workflows that span multiple requests (multi-step forms, approval chains), use sessions: ```typescript handler: (data, context) => { // Store progress context?.setSession("rest", { step: 2, orderId: data.orderId }); // Retrieve later const session = context?.getSession("rest"); return Ok({ currentStep: session?.step }); }, ``` ### Organizing Complex Domains Services keep workflows modular. Each service groups related micro-actions that each do one thing well. Complex workflows compose these small actions via hooks or orchestrator actions that call domain logic directly: ``` services/ orders/ validate-order.ts # validates order data process-payment.ts # handles payment logic create-order.ts # orchestrates the full flow cancel-order.ts # handles cancellation inventory/ check-stock.ts # stock availability check reserve-items.ts # temporary hold on items notifications/ send-confirmation.ts # order confirmation email ``` --- ## What if the service-action structure doesn't fit my domain? It does. The service-action model maps directly to domain-driven design, and every backend operation — regardless of complexity — reduces to "which domain?" and "what operation?". ### Think in Domains and Operations A **service** is a bounded context: users, orders, payments, notifications. An **action** is an operation within that context: create, update, validate, process. If you find yourself fighting the structure, you're likely thinking in terms of routes or CRUD endpoints. Shift the mental model: | Traditional Thinking | Nile Thinking | |---------------------|---------------| | `POST /orders` | service: `orders`, action: `create` | | `GET /orders/:id/status` | service: `orders`, action: `getStatus` | | `POST /orders/:id/refund` | service: `orders`, action: `refund` | | `POST /checkout` | service: `checkout`, action: `process` | Every HTTP endpoint you would normally create maps to a service + action pair. ### Coarse and Fine-Grained Actions Actions can be as granular or as broad as your domain requires. A simple CRUD operation and a complex multi-step workflow are both just actions: ```typescript // Fine-grained: single responsibility export const validateEmail: Action = createAction({ name: "validateEmail", description: "Check if email is valid and not taken", handler: (data) => { /* focused logic */ }, }); // Coarse-grained: orchestrates multiple steps export const registerUser: Action = createAction({ name: "registerUser", description: "Full user registration flow", handler: async (data) => { // Calls validation, creates user, sends welcome email // All within one handler — it's just a function }, }); ``` ### Cross-Cutting Concerns Logic that applies across services (logging, authorization, rate limiting, audit trails) goes in global hooks: ```typescript const server = createNileServer({ services: [/* ... */], onBeforeActionHandler: async (request, context) => { // Runs before every action in every service await auditLog(request.service, request.action, context.getUser()); return Ok(request.payload); }, onAfterActionHandler: async (result, context) => { // Runs after every action in every service trackMetrics(context.hookContext?.actionName, result.isOk); return result; }, }); ``` ### Cross-Service Composition via Hooks An action in one service can reference actions in another via hooks, without tight coupling: ```typescript export const createOrder: Action = createAction({ name: "createOrder", hooks: { before: [ { service: "auth", action: "verifyPermissions", isCritical: true }, { service: "inventory", action: "reserveItems", isCritical: true }, ], after: [ { service: "notifications", action: "sendOrderEmail", isCritical: false }, ], }, handler: (data) => Ok({ order: data }), }); ``` The services remain independent. The composition is declared at the action level, not embedded in business logic. ### The Constraint Is the Strength The service-action structure forces clean domain separation. You cannot create a handler that lives outside a service. You cannot create an operation that isn't an action. This prevents the common patterns that make backends hard to maintain: scattered route handlers, middleware spaghetti, and implicit dependencies between endpoints. Every operation in your system has a clear address: `service.action`. Every piece of business logic has a defined home. This makes the codebase navigable, testable, and composable by default. --- ## Why not use GraphQL instead? GraphQL is powerful but introduces a significant learning curve: query syntax, resolvers, dataloaders, schema stitching, and specialized caching strategies. For teams already productive with request/response patterns, that overhead often isn't justified. Nile keeps the familiar mental model — send a request, get a response — while eliminating the boilerplate of traditional REST. You don't need to learn a new query language or tooling ecosystem. The `explore` and `schema` intents give you the introspection benefits that make GraphQL attractive, without the complexity tax. If your team is already invested in GraphQL and it's working, keep using it. Nile is for teams that want structured, discoverable APIs without leaving the request/response paradigm. --- ## Why not use tRPC for type safety? tRPC's type safety is excellent, but it requires a monorepo or complex type-sharing setup. If your frontend and backend live in separate repositories — which is common — tRPC's main advantage disappears while the infrastructure complexity remains. Nile's approach is different: the `schema` intent exports Zod validation schemas as JSON Schema over the wire. The `@nilejs/client` provides type-safe invocations when you pass generated types as a generic. You get compile-time checking on service names, action names, and payload shapes without coupling your repositories. ```typescript import { createNileClient } from "@nilejs/client"; import type { ServicePayloads } from "./generated/types"; const nile = createNileClient({ baseUrl: "/api" }); // Full autocomplete and type checking — no monorepo required await nile.invoke({ service: "tasks", action: "create", payload: { title: "Buy milk" }, }); ``` --- ## Why not stick to pure REST? Business logic doesn't fit cleanly into HTTP verbs. Operations like `calculateShipping`, `processPayment`, or `generateReport` aren't resource updates — they're actions. Forcing them into PUT/POST/PATCH is artificial and obscures what the operation actually does. Nile makes operations explicit. `service: "shipping", action: "calculate"` is clearer than `POST /shipping/calculations` and wondering whether it creates a resource or just computes a value. Business clarity over HTTP purity. For simple CRUD where REST mapping is natural, Nile still works — you just name your actions `create`, `get`, `update`, `delete`. The difference is you're not constrained to that model when your domain outgrows it. --- ## Doesn't using POST for everything violate HTTP semantics? Yes, and that's a deliberate trade-off. Nile prioritizes consistent patterns and business clarity over HTTP semantic correctness. For internal APIs where you control both client and server, the benefits of explicit action-based routing outweigh HTTP-level caching. Application-level caching (Redis, in-memory stores, database query optimization) is more appropriate for complex business operations anyway — most enterprise logic involves multiple data sources and calculations that don't cache well at the HTTP transport layer. The single-endpoint model also simplifies infrastructure: load balancers, API gateways, and proxies only need to handle one route. --- ## How does Nile compare to JSON-RPC and traditional REST? | Feature | JSON-RPC | Nile | Traditional REST | |---------|----------|------|-----------------| | Discovery | External docs | Built-in `explore` intent | HATEOAS (rarely implemented) | | HTTP Methods | POST only | POST only | Full verb semantics | | URL Structure | Single endpoint | Single endpoint | Resource-based routes | | Validation | Manual | Built-in Zod schemas | Manual or OpenAPI | | Type Safety | Manual | Schema-driven client types | Manual or codegen | | Error Format | JSON-RPC error codes | Consistent `{ status, message, data }` | Varies per endpoint | Nile borrows RPC's explicit operations and adds REST-like discoverability. The `explore` intent replaces the need for external API documentation, and `schema` provides machine-readable type information that JSON-RPC doesn't offer. --- ## So is Nile just RPC? Not exactly. Nile uses RPC-style communication — named operations with typed payloads over a single endpoint — but it's a backend framework, not a protocol. RPC (JSON-RPC, gRPC, etc.) defines how messages are formatted and transported. Nile defines how you **build and organize** your backend. The RPC-like request format is just the transport interface. Behind it sits a full application framework: an execution engine with O(1) routing, a hook pipeline for composable middleware, built-in JWT authentication, Zod validation, context management, session handling, structured logging, and a typed client SDK. Calling Nile "RPC" is like calling Express "HTTP" — technically accurate at the transport level, but it misses everything the framework does above that layer. The better label is **service-action framework**. You define services (domains) and actions (operations). The framework handles everything else: discovery, validation, auth, execution pipelines, error handling, and the consistent response format. The single-POST interface is an implementation detail, not the identity. --- ## How do I handle caching? An action is just an action — whether it reads or writes is entirely up to you. Caching happens at the layers where it makes sense: **Client-side:** Tools like React Query (TanStack Query) for React, SWR, or a wrapped fetch with caching logic work perfectly. You cache based on the `service + action + payload` combination as your cache key. Browser-level HTTP caching (ETags, Cache-Control) isn't available since everything is POST, but Nile is primarily used in systems like dashboards and internal tools where that rarely matters. **Server-side:** Cache in your handlers or at the data layer. In-memory caches, Redis, database query optimization — all work the same as any backend: ```typescript handler: async (data, context) => { const cacheKey = `products:${data.category}`; const cached = await redis.get(cacheKey); if (cached) return Ok(JSON.parse(cached)); const products = await fetchProducts(data.category); await redis.set(cacheKey, JSON.stringify(products), "EX", 300); return Ok({ products }); }, ``` It's a trade-off. You lose automatic browser caching in exchange for a simpler, consistent API surface. For the use cases Nile targets — business logic, dashboards, internal tools, multi-step workflows — application-level caching is more appropriate and more controllable anyway. ### Do you differentiate between read and mutate actions? No. Nile does not distinguish between read and write operations at the protocol level. Every action goes through `POST {baseUrl}/services` regardless of whether it reads data or mutates it. Splitting reads into `GET` with query params and mutations into `POST` would enable HTTP-level caching and CDN compatibility, but it would also mean two different request formats, two different parsing paths, and payload limitations from URL length constraints for complex query shapes. The single-POST model keeps the protocol uniform and the implementation simple. If HTTP-level caching is critical to your use case (e.g., a public content API behind a CDN), Nile may not be the right tool — see [When should I NOT use Nile?](#when-should-i-not-use-nile). --- ## How do I handle idempotency without PUT/DELETE? At the application level, which is where idempotency should live anyway. HTTP verb semantics give you theoretical idempotency guarantees that rarely hold in practice for complex operations. Practical approaches: - **Idempotency keys** in the payload — clients send a unique key, the server deduplicates - **Database constraints** — unique indexes prevent duplicate resource creation - **Action + resource ID** — the combination of `service.action` and the target resource naturally identifies the operation - **Conditional checks** in handlers — check current state before applying changes ```typescript handler: async (data, context) => { // Idempotency via payload key const existing = await findByIdempotencyKey(data.idempotencyKey); if (existing) return Ok({ order: existing }); // Already processed const order = await createOrder(data); return Ok({ order }); }, ``` --- ## How do I version my API? Multiple strategies, all straightforward with the action model: **URL versioning** for major breaking changes: ```typescript rest: { baseUrl: "/api/v1" } // v1 rest: { baseUrl: "/api/v2" } // v2 ``` **Action evolution** — add new actions, deprecate old ones: ```typescript // Keep the old action working createAction({ name: "createUser", ... }); // Add the new version alongside it createAction({ name: "createUserV2", ... }); ``` **Payload evolution** — add optional fields, maintain backward compatibility. Since payloads are validated with Zod, you can use `.optional()` and `.default()` to evolve schemas without breaking existing clients. **Service splitting** — break large services into focused ones as your domain grows. The `explore` intent lets clients discover what's available, so renaming or splitting services is transparent. The action-based model actually makes versioning easier than endpoint-based APIs — adding a new action never conflicts with existing ones, and the `schema` intent always reflects the current state. --- ## How does testing work? The consistent request/response format simplifies testing significantly. **Unit testing actions** — handlers are plain functions that take data and return Results: ```typescript import { describe, it, expect } from "vitest"; describe("createTask", () => { it("creates a task with valid data", () => { const result = createTaskHandler({ title: "Test" }); expect(result.isOk).toBe(true); expect(result.value).toEqual({ task: { id: expect.any(String), title: "Test" } }); }); it("rejects empty title", () => { const result = createTaskHandler({ title: "" }); expect(result.isErr).toBe(true); }); }); ``` **Integration testing** — one endpoint to mock, standardized responses: ```typescript const response = await app.request("/api/services", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ intent: "execute", service: "tasks", action: "create", payload: { title: "Test" }, }), }); const json = await response.json(); expect(json.status).toBe(true); ``` **Client testing** — mock the single endpoint and test all service interactions: ```typescript const nile = createNileClient({ baseUrl: "http://mock/api" }); const { error, data } = await nile.invoke({ service: "tasks", action: "create", payload: { title: "Test" }, }); ``` No route table to replicate, no verb-specific mocks, no middleware stacking per test. --- ## What runtimes does Nile support? Nile runs on **Bun** and **Node.js**. The HTTP layer is built on [Hono](https://hono.dev), which supports both runtimes natively. - **Bun** — recommended for development and production. Fastest startup, native TypeScript support, built-in test runner. - **Node.js** — fully supported via `@hono/node-server`. Use this when deploying to environments that don't support Bun. Runtime-specific features (like static file serving) are handled via dynamic adapter imports — the same code works on both runtimes without configuration changes. --- ## What databases work with Nile? Nile is database-agnostic. The framework doesn't prescribe a database or ORM — you pass your database instance as a resource and access it via context: ```typescript const server = createNileServer({ services: [/* ... */], resources: { database: myDrizzleInstance, // or Prisma, Kysely, raw pg, etc. }, }); ``` The built-in `createModel` utility provides a typed CRUD model factory for [Drizzle ORM](https://orm.drizzle.team), but this is optional. You can use any database client or ORM — just pass it through resources and access it in your handlers. --- ## Does Nile support file uploads? Yes. Nile handles multipart `FormData` requests natively. The REST layer detects the content type and parses files automatically. Server-side, uploaded files arrive in the action payload alongside regular fields. You can configure validation rules for file size, count, and MIME types. The `@nilejs/client` provides an `upload` method and a `buildFormData` utility for constructing upload requests: ```typescript const { error, data } = await nile.upload({ service: "media", action: "uploadImage", payload: formData, }); ``` See the upload configuration in your server's `RestConfig` for validation options. --- ## When should I NOT use Nile? Nile is not the right fit for every project: - **Public APIs where REST conventions are expected** — if your API consumers expect standard REST verbs and resource-based URLs, Nile's single-endpoint model will confuse them - **Simple CRUD apps** — if your entire backend is basic resource operations with no business logic, a REST framework with auto-generated routes is faster to set up - **Teams deeply invested in GraphQL** — if GraphQL is working and the team knows it, switching adds friction with no clear gain - **Performance-critical APIs that rely on HTTP caching** — CDN-level caching with ETags and Cache-Control headers doesn't apply to a single POST endpoint - **Cross-organization APIs** — when API consumers are external teams with their own tooling expectations, standard REST or GraphQL is the safer choice Nile excels at internal APIs, complex business logic, multi-service architectures, and teams that want structured, discoverable backends without boilerplate. --- ## How do I migrate from an existing REST API? Gradual migration works best: 1. **Start new features with Nile** — new services and actions are built in Nile from day one 2. **Wrap existing endpoints** — create Nile actions that proxy to your existing REST handlers during transition 3. **Migrate high-change services first** — services with frequent updates benefit most from the action model 4. **Keep stable CRUD services as-is** — if it works and rarely changes, there's no urgency to migrate 5. **Use an API gateway** — route `/api/v2/services` to Nile and legacy routes to the old server during transition The action-based model doesn't require an all-or-nothing switch. You can run Nile alongside existing infrastructure and migrate incrementally. --- ## How steep is the learning curve? Minimal. If you can write a function that takes input and returns output, you can write a Nile action. The core concepts are: - **Service** = a named group of related operations - **Action** = a function with a name, optional validation, and a handler - **Intent** = what you want to do (execute, explore, schema) That's it. No decorators, no class hierarchies, no dependency injection containers, no module systems to learn. Define actions as functions, group them into services, start the server. ```typescript // This is a complete Nile action export const hello = createAction({ name: "hello", description: "Say hello", handler: (data) => Ok({ message: `Hello, ${data.name}` }), }); ``` Most developers are productive within an hour because they're already thinking in terms of functions and operations. The framework just gives that mental model a consistent structure. --- ## How is Nile different from MCP (Model Context Protocol)? Nile is a backend framework. MCP is a protocol for connecting AI models to tools. They emerged around the same time from different starting points and landed on similar-ish patterns — named operations, structured payloads, discovery — but they solve different problems. ### The Core Difference Nile builds your backend. It's your API, your business logic, your auth, your validation, your hooks pipeline. Whether you ever expose it to AI is entirely up to you. MCP defines how an AI model calls external tools during a conversation. It's a communication protocol, not a framework — it doesn't handle auth, validation pipelines, database access, error logging, or any of the things a real backend needs. ### Why the Similarity? Both Nile and MCP converged on the same structural insight: named operations with typed inputs are more expressive than verb-based routing. In Nile this is `service.action` with Zod schemas. In MCP this is tool definitions with JSON Schema parameters. The pattern is the same because the underlying idea — explicit operations over implicit conventions — is just a good idea. ### AI Integration Without MCP Here's the practical point: if you build your backend with Nile, you already have everything an AI agent needs to interact with it. Every action has a name, a description, and a typed schema. The `explore` intent returns what's available. The `schema` intent returns the exact input shapes. An AI model can discover your API, understand what each operation does, and call it — all through the same endpoint your frontend uses. You don't need MCP as a middleman. Your Nile backend *is* the tool interface. When you want to expose actions to AI, you just point the model at your API — the metadata is already there. ```typescript // This action is simultaneously: // - A backend endpoint for your frontend // - A tool an AI agent can discover and call export const searchProducts: Action = createAction({ name: "searchProducts", description: "Search products by name, category, or price range", validation: z.object({ query: z.string().optional(), category: z.string().optional(), minPrice: z.number().optional(), maxPrice: z.number().optional(), }), handler: async (data, context) => { const results = await findProducts(data); return Ok({ products: results }); }, }); ``` An AI model reading this action's schema from the `explore` or `schema` intent gets everything it needs: the operation name, what it does, and the exact parameters it accepts. No adapter layer, no MCP server, no separate tool definitions to maintain. ### When You'd Use Both If you're building in an ecosystem that requires MCP specifically (e.g., Claude desktop integrations, or tools that only speak MCP), you could write a thin MCP server that proxies to your Nile backend. The mapping is almost 1:1 — action names become tool names, Zod schemas become JSON Schema parameters. But that's an integration choice, not a requirement. ### Vibe Coding Ready Nile is indexed on [Context7](https://context7.com/nile-js/nile), an MCP server that feeds up-to-date library documentation directly into AI coding assistants. This means any AI tool with Context7 MCP support (Cursor, Windsurf, Copilot, Claude Code, etc.) can pull Nile's full documentation — services, actions, hooks, auth, uploads, client SDK — into context while generating code. You don't need to paste docs or explain the framework to your AI. It already knows Nile. **Bottom line:** Build your backend with Nile. If AI needs to talk to it, the structure is already there. MCP is a protocol for a specific use case. Nile is the framework that builds the thing the protocol talks to. --- url: /nile/guide/others/framework-comparison.md --- # Framework Comparison > Objective comparison based on actual codebase analysis of the Nile project. --- ## What Nile IS Nile is a **TypeScript-first, service-action backend framework** built on **Hono** (HTTP), **Zod** (validation), and **slang-ts** (Result pattern). It uses a **single POST endpoint** where all communication flows through `POST /services` with an `intent` field (`explore`, `execute`, `schema`). Nile is not REST. It's not RPC. It's not trying to fit into either category. The single-POST interface is a transport detail — behind it sits a full application framework with an execution engine, hook pipelines, built-in JWT auth, file upload handling, structured logging, and a typed client SDK. The better label is **service-action framework**: you define domains (services) and operations (actions), and the framework handles discovery, validation, auth, execution, and consistent response formatting. The architecture emerged around the same time as Anthropic's MCP (Model Context Protocol) and landed on similar patterns — named operations, typed inputs, built-in discovery — from a completely different starting point. MCP is a protocol for AI-to-tool communication. Nile is a framework for building backends. The structural similarity means that a Nile backend is inherently AI-agent-ready without needing MCP as a middleman: every action already has a name, description, and typed schema that any agent can discover and invoke through the same endpoint applications use. Key architectural choices: - **Functional, factory-based** — no classes, no decorators, no DI containers - **Result pattern everywhere** — `Ok(data)` / `Err(message)`, no try/catch - **Service -> Action hierarchy** — services group actions; actions are plain objects with `name`, `handler`, optional `validation` (Zod), optional `hooks` - **AI-agent ready by default** — every action with a Zod schema auto-exports JSON Schema via the `schema` intent. No adapter layer, no MCP server, no separate tool definitions to maintain - **Honest trade-offs** — no HTTP verb semantics means no browser-level caching. Nile targets dashboards, internal tools, and business logic APIs where application-level caching is more appropriate - **Monorepo**: `@nilejs/nile` (core), `@nilejs/client` (zero-dep typed client), `@nilejs/cli` (scaffolding/codegen) --- ## Comparison Table | Aspect | **Nile** | **tRPC** | **GraphQL** | **Elysia** | **NestJS** | |---|---|---|---|---|---| | **Identity** | Service-action framework | End-to-end TS type-safe RPC | Query language + runtime | Bun-native HTTP framework | Enterprise OOP framework | | **Philosophy** | Functional, domain-driven actions with built-in AI discoverability | Zero-codegen type safety across client/server | Flexible query language for complex data graphs | Raw performance, Bun-native | Angular-inspired enterprise patterns | | **Paradigm** | Functional factories | Functional procedures | Schema-first or code-first | Functional with method chaining | Class-based, decorator-heavy OOP | | **Transport** | Single POST endpoint, intent-based | HTTP/WebSocket, procedure-based | Single POST endpoint, query-based | Full HTTP method routing | Full HTTP method routing | | **Type Safety** | Zod schemas + CLI codegen -> typed client (no monorepo required) | Inferred types, zero codegen, monorepo-tight | Schema types + codegen (e.g., graphql-codegen) | Zod/TypeBox schema inference | Decorators + class-validator | | **Routing** | O(1) pre-computed `service->action` map lookups | Procedure routers, nested | Single endpoint, resolver-based | Radix-tree HTTP router (Bun-optimized) | Express/Fastify controller decorators | | **Auth** | Built-in JWT (header/cookie), per-action `isProtected`, RBAC via hooks | None (bring your own) | None (bring your own) | Guard system | Guards + Passport integration | | **Middleware** | Global before/after hooks + per-action hooks, sequential pipeline with `isCritical` control | Middleware via context | Resolver middleware, directives | Lifecycle hooks, derive, guard | Guards, interceptors, pipes, filters | | **Error Handling** | Result pattern (`Ok`/`Err`), `handleError` for logged errors, `safeTry` crash safety | Result-like with error formatting | Error extensions in responses | Throw or return errors | Exception filters, throw HttpException | | **Validation** | Zod (runtime), auto-generated from Drizzle tables | Zod (inferred at compile time) | Schema-level type checking | Zod/TypeBox with type inference | class-validator decorators | | **File Uploads** | Built-in multipart FormData parsing with validation (size, count, MIME) | None (bring your own) | multipart via Apollo Upload | Multipart via Elysia plugin | Multer integration | | **Caching** | Application-level (Redis, in-memory). No HTTP-level caching (deliberate trade-off). Client-side via React Query/SWR | Application-level | HTTP caching + persisted queries | Full HTTP caching (ETags, Cache-Control) | Full HTTP caching | | **DX** | Simple: define action -> register in service -> done. CLI scaffolds | Excellent: autocomplete across client/server | Steep learning curve, powerful tooling | Fast setup, ergonomic API | Heavy boilerplate, comprehensive docs | | **Performance** | Hono + Bun + O(1) lookups — no route matching overhead at any scale. Benchmarks pending | Lightweight runtime | Resolver overhead, N+1 risk | Fastest (Bun-native benchmarks) | Moderate (abstraction layers) | | **Scalability** | Immutable after boot, duplicate detection at startup | Monorepo-biased | Excellent for distributed/federated | Promising for edge/serverless | Enterprise-proven microservices | | **AI/Agent Support** | **Built-in**: `explore` + `schema` intents = complete tool interface. No MCP layer needed | Not built-in | Introspection exists but not agent-targeted | Not built-in | Not built-in | | **DB Integration** | Optional Drizzle-based `createModel` with auto-CRUD, pagination, transactions | None (bring your own) | None (bring your own) | None (bring your own) | TypeORM/Prisma/Sequelize integrations | | **Client** | Zero-dep `@nilejs/client` with typed payloads, upload support, discovery methods | Built-in typed client (monorepo) | Apollo Client, urql, Relay | Eden treaty (typed) | No official client | | **Runtime** | Bun + Node.js (via @hono/node-server) | Node.js, edge runtimes | Any (runtime-agnostic) | Bun only | Node.js (Express/Fastify) | | **Maturity** | Early stage — core solid (auth, uploads, hooks, validation, client), WebSocket and streaming not yet implemented | Mature, widely adopted | Very mature, industry standard | Growing rapidly | Very mature, enterprise-proven | --- ## Nile's Unique Strengths 1. **AI-Agent Ready Without MCP** — The `explore` and `schema` intents make every Nile backend a complete tool interface for AI agents. An LLM can discover available actions, understand their input schemas, and invoke them — all through the same endpoint applications use. No MCP server, no adapter layer, no separate tool definitions. The structural similarity to MCP is no coincidence — both converged on named operations with typed inputs as the right abstraction — but Nile is a framework that builds the backend, not a protocol that talks to it. 2. **Radical Simplicity** — No decorators, no DI, no classes. An action is just `{ name, handler, validation }`. A service is just `{ name, actions[] }`. The learning curve is minimal — most developers are productive within an hour. 3. **Result Pattern Consistency** — The entire pipeline uses `Ok`/`Err` from top to bottom. `handleError` adds logged, traceable errors for runtime failures. `safeTry` catches uncaught exceptions. No exception-based control flow anywhere. Error paths are explicit and predictable from handler to client. 4. **Hook System** — Before/after hooks at global and per-action level, with `isCritical` flag controlling pipeline continuation. Hooks reference other registered actions by `service.action` address, enabling cross-service composition without coupling. 5. **Built-in Auth** — JWT authentication configured once at the server level, enforced per-action via `isProtected`. Custom authorization (RBAC, API keys) via `onBeforeActionHandler` hooks with access to verified identity. No separate auth middleware to wire up. 6. **Database Utilities** — `createModel` generates typed CRUD operations from Drizzle tables with auto-validation, cursor/offset pagination, and transaction variants out of the box. Database-agnostic for those who prefer other ORMs. 7. **Honest Trade-offs** — Nile is upfront about what it doesn't do: no HTTP verb semantics, no browser-level caching, no per-endpoint route matching. These are deliberate design decisions, not missing features. The framework targets dashboards, internal tools, business logic APIs, and AI integration — use cases where these trade-offs are strengths. --- ## Current Gaps - **WebSocket/RPC transports** — placeholder types only, not yet implemented - **Streaming** — no SSE, WebSocket, or chunked transfer support - **Actions immutable after boot** — no dynamic registration of services/actions at runtime - **Handler data** typed as `Record` — requires manual casting inside handlers (Zod validates shape, but the handler signature doesn't reflect it) - **No HTTP-level caching** — single POST endpoint means no ETags, Cache-Control, or CDN-friendly GET routes. Application-level caching required. - **No read/write action distinction** — all actions go through POST regardless of intent. No GET-with-query-params path for read operations --- ## When to Choose What | Scenario | Best Pick | Why | |---|---|---| | Full-stack TS monorepo, rapid iteration | **tRPC** | Unmatched type inference across client/server with zero codegen | | Complex multi-consumer data APIs, federated graphs | **GraphQL** | Flexible queries, schema federation, mature ecosystem | | Raw performance, edge/serverless, Bun-only | **Elysia** | Bun-native optimizations, fastest benchmarks | | Enterprise microservices, structured teams | **NestJS** | Battle-tested patterns, comprehensive DI, proven at scale | | Public APIs behind CDNs, HTTP caching required | **Elysia / NestJS** | Full HTTP verb semantics, GET routes for cacheable reads | | AI-agent-ready APIs, functional simplicity | **Nile** | Built-in discovery + schema = complete tool interface without MCP | | Dashboards, internal tools, business logic APIs | **Nile** | Action model maps naturally to business operations, minimal boilerplate | | Small teams wanting minimal boilerplate + DB utilities | **Nile** | `createAction` -> `createService` -> done. Optional `createModel` for Drizzle CRUD | | Separate frontend/backend repos with type safety | **Nile** | Schema-driven client types without monorepo coupling | --- ## Verdict Nile is a **service-action framework** — not REST, not RPC, not trying to be either. It occupies a distinct niche: structured, discoverable backends built from named operations with typed inputs, where the same API surface serves both applications and AI agents without an adapter layer. The core is solid: JWT auth, file uploads, Zod validation, hook pipelines, O(1) engine routing, Result pattern error handling, duplicate detection, cross-runtime support (Bun + Node), a zero-dep typed client with upload support, and built-in discovery via `explore`/`schema` intents. The 318-test suite covers the implementation thoroughly. The honest gaps are WebSocket support and streaming — both planned but not implemented. The single-POST model means no HTTP-level caching, which rules Nile out for public CDN-cached APIs. Handler data typing (`Record`) requires manual casting despite Zod validation. The competitive positioning is clear: - tRPC owns monorepo type inference. Nile offers type safety without the monorepo requirement. - GraphQL owns complex data graphs. Nile targets operational APIs, not query-heavy data fetching. - Elysia owns raw Bun performance. Nile's O(1) routing is fast but doesn't compete on benchmark territory. - NestJS owns enterprise patterns. Nile trades OOP structure for functional simplicity. Where Nile stands alone is the AI integration story. A Nile backend is a complete tool interface out of the box — discoverable, typed, invocable — without MCP, without OpenAPI generation, without any additional layer. As AI-agent integration becomes standard for backend services, this is a genuine structural advantage that the other frameworks would need bolted on. --- url: /nile/guide/others/llms-txt.md --- # LLMs.txt [View LLMs.txt](/llms.txt) ## Context7 MCP Nile is indexed on [Context7](https://context7.com/nile-js/nile) — an MCP server that feeds up-to-date library documentation directly into AI coding assistants. Any AI tool with Context7 MCP support (Cursor, Windsurf, Copilot, Claude Code, etc.) can pull Nile's full documentation into context while generating code. This means your AI assistant already knows how to work with Nile — services, actions, hooks, auth, uploads, the client SDK — without you needing to paste docs or explain the framework. --- url: /nile/guide/others/llms-full-txt.md --- # LLMs Full [View LLMs Full](/llms-full.txt) --- url: /nile/guide/roadmap.md --- # Roadmap What's coming next for Nile. --- - **WebSocket Transport** — Real-time bidirectional communication using the same service/action model. Subscribe to action results, push notifications. - **Streaming Responses** — SSE and chunked transfer for long-running actions (AI completions, large dataset exports). - **Performance Benchmarks** — Formal benchmark suite measuring O(1) action dispatch, throughput comparisons against endpoint-based frameworks (Express, Fastify, NestJS), and Bun vs Node runtime performance across real-world workloads. --- *This roadmap reflects current priorities and may change. For the latest status, check the repository issues and discussions.* --- url: /nile/index.md ---