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.jsmaps to/api.routes/api/orders/[id].server.jsmaps to/api/orders/:id.routes/api/files/[...path].server.jscaptures 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
/@niraland/assets
are reserved. Trailing slashes are equivalent. Malformed URL encoding returns 400.
Request context
Handlers receive { request, url, params, locals, session, user }:
requestis a native Request: use json(), text(), arrayBuffer() or formData().
The body is buffered once, preserving bytes for signature verification.
urlis a URL with searchParams.paramscontains decoded route parameters.localscomes from hooks.js. Middleware guards and short-circuit responses still apply.sessionsupports 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
- 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.