Docs

HTTP API routes

Build HTTP APIs in Niral with file-based server routes, native Request and Response objects, method handlers, validation and signed webhook examples.

On this page 5 sections

Export HTTP methods from a routes/**/*.server.js file. Endpoints run only on the server and return native Web Response objects. No component or browser bundle is required.

Start an endpoint

niral add api
niral dev

The recipe creates routes/api/index.server.js at /api, with GET and POST handlers. It refuses to overwrite an existing route. API-only projects also support niral build and niral start.

// routes/api/orders/[id].server.js
export function GET({ params, url, user }) {
  if (!user()) return Response.json({ error: "Sign in first" }, { status: 401 });
  return Response.json({ id: params.id, format: url.searchParams.get("format") });
}

export async function PATCH({ params, request, user }) {
  if (!user()) return Response.json({ error: "Sign in first" }, { status: 401 });
  const changes = await request.json();
  if (typeof changes?.title !== "string") {
    return Response.json({ error: "title is required" }, { status: 422 });
  }
  return Response.json({ id: params.id, title: changes.title });
}

This example validates and echoes a change. Persist updates in your database and check ownership against user().id before reading or changing a record.

Routing and methods

  • routes/api/index.server.js maps to /api.
  • routes/api/orders/[id].server.js maps to /api/orders/:id.
  • routes/api/files/[...path].server.js captures the remaining path, including an empty suffix.
  • Static endpoint segments win over parameters; parameters win over catch-alls.
  • Export GET, HEAD, POST, PUT, PATCH, DELETE or OPTIONS. Other methods receive

405 with an Allow header. HEAD falls back to GET without sending its body; explicit HEAD overrides it. OPTIONS defaults to 204 with Allow.

  • Endpoints run before static files and pages, after middleware. A page and endpoint

cannot own the same route pattern, even with different parameter names. Keep API routes under a distinct prefix to avoid overlapping page patterns.

  • Catch-alls must be last, parameter names cannot repeat, and /@niral and /assets

are reserved. Trailing slashes are equivalent. Malformed URL encoding returns 400.

Request context

Handlers receive { request, url, params, locals, session, user }:

  • request is a native Request: use json(), text(), arrayBuffer() or formData().

The body is buffered once, preserving bytes for signature verification.

  • url is a URL with searchParams. params contains decoded route parameters.
  • locals comes from hooks.js. Middleware guards and short-circuit responses still apply.
  • session supports get(key, default), set(key, value), delete(key), all().

Middleware and endpoint writes share one signed session and persist in Set-Cookie.

  • user() returns the session user or null. Authentication is explicit, not automatic.

The default body limit is 1 MiB; set the positive-integer NIRAL_MAX_BODY byte limit server-side to change it. Oversized requests return 413; JSON syntax errors return

  1. Unhandled failures return 500 with details only in dev; production logs retain

the exception. Validate application inputs and return your own 4xx responses.

Unsafe methods reject an Origin whose host differs from the request URL. Set NIRAL_ORIGIN=https://your-domain.example behind a reverse proxy to provide the canonical request URL; forwarded headers are not trusted automatically. Cross-origin writes are not enabled by default. Server-to-server webhooks without Origin are supported. Always authenticate API clients and verify webhook signatures.

Signed webhooks

// routes/api/webhook.server.js
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST({ request }) {
  const raw = Buffer.from(await request.arrayBuffer());
  const expected = createHmac("sha256", process.env.WEBHOOK_SECRET).update(raw).digest();
  const signature = request.headers.get("x-signature") ?? "";
  if (!/^[0-9a-f]{64}$/.test(signature) ||
      !timingSafeEqual(expected, Buffer.from(signature, "hex"))) {
    return new Response(null, { status: 401 });
  }
  const event = JSON.parse(raw.toString("utf8"));
  return Response.json({ received: event.type });
}

Declare WEBHOOK_SECRET in hooks.js's required env list. Match your provider's signing format; add timestamp validation and persistent event-ID deduplication for replay protection before processing real events. The example demonstrates byte-exact HMAC verification.

Responses and private imports

Return Response.json(), a Response containing text or binary bytes, or a Response with a ReadableStream. Status, headers and multiple Set-Cookie values are preserved; streaming respects backpressure and disconnects. Set session values before returning the Response, not during streaming. Responses default to Cache-Control: no-store.

Relative ESM imports with explicit .js, .mjs or .json extensions and Node built-ins are supported. JSON imports use Node's import attributes. Literal dynamic imports are supported; computed dynamic imports and imports in template expressions are not. CommonJS loading and package imports are not part of the endpoint build contract.

The endpoint dependency graph is copied into the release's private server directory, never public assets. Imported helpers become server-only even without a .server.js suffix. Do not share those helpers with browser modules. Build validates syntax and hashes dependencies; dev reloads the graph when any imported source changes. import.meta.url refers to the copied module. Use a configured absolute data path for persistent storage, never a path relative to a release module.