GioJSdocs
On this page

GET, POST, PUT, PATCH, DELETE

The HTTP method handlers a route.ts exports: what they receive, what they may return, and how each method is answered.

app/api/items/[id]/route.ts
import { notFound, type RouteHandler } from '@gio.js/core';
import { db } from '../../../../lib/db.server.ts';

export const GET: RouteHandler<'/api/items/:id'> = async (req) => {
  const item = await db.items.find(req.params.id);
  if (item === null) notFound();                  // {"error":"Not Found"}, 404
  return item;                                    // JSON, 200
};

export const DELETE: RouteHandler<'/api/items/:id'> = async (req) => {
  await db.items.delete(req.params.id);
  return null;                                    // 204
};

A route.ts (or route.js) turns its folder into an endpoint. Each named export GET, POST, PUT, PATCH or DELETE that is a function handles that method. They can be declared as functions or as constants, sync or async.

Reference

Parameters

Each handler receives one GioRequest<Route>:

FieldTypeDefaultDescription
methodstring-The request method. When the GET handler answers a HEAD request, it is 'HEAD'.
pathstring-The routed path, without the query string.
paramsParamsOf<Route>-The dynamic segments. A catch-all is one /-joined string.
queryRecord<string, string>-The query string, one value per name: the last one when a name repeats.
headersRecord<string, string>-The request headers, names lowercase.
cookiesRecord<string, string>-The Cookie header, parsed.
bodystring | null-The raw body: UTF-8 text, or base64 when bodyBase64 is true. null without one.
bodyBase64boolean-Whether body is base64 (a binary upload).
json()T-Parses a body sent as application/json or application/*+json. Another content type throws UnsupportedMediaTypeError (415 unless caught). An empty or non-UTF-8 body, or JSON that does not parse, throws MalformedBodyError (400 unless caught).
formData()Promise<FormData>-Parses application/x-www-form-urlencoded and multipart/form-data; files are File objects. Another content type is a 415, a body that does not parse a 400 (MalformedBodyError).
localestring | undefined-The request locale, with [i18n].
ipstring | undefined-The client's address; behind a proxy only when it is in [server] trusted_proxies. Never read x-forwarded-for yourself.
scheme, hoststring | undefined-How the client addressed the server. host is whatever the client sent unless a proxy pins it: fine for display, never for a security decision.
requestIdstring | undefined-The response's X-Request-Id, also on every log line.

Returns

Return valueResponse
A web ResponseIts status, headers and body. Binary bodies arrive byte for byte; each Set-Cookie is sent as its own header. Without a Content-Type, text/plain; charset=utf-8. A ReadableStream body streams.
A GioEventStreamServer-Sent Events (text/event-stream), from any method. A browser's EventSource sends GET; fetch() can read a stream from a POST.
redirect(), returned or thrownIts status (303 by default), its headers and Location with the URL as written, an empty body.
null or undefined204 No Content.
Any other value200, the value as JSON (application/json; charset=utf-8).

Behavior

  • HEAD is answered by the GET handler, without the body. HEAD and OPTIONS cannot be exported: only the five methods above are read.
  • Other methods answer 405 with {"error":"Method Not Allowed"} and an Allow header listing what the file exports (plus HEAD when it exports GET). OPTIONS is answered the same way.
  • A page in the same folder serves the methods the route.ts does not export: GET/HEAD render the page, and POST runs its action. A 405 from that folder lists both files' methods.
  • Errors. notFound() answers 404 {"error":"Not Found"}. A body error from json() or formData() answers 415 or 400 (see request body errors) and is not logged. Any other thrown error answers 500 {"error":"Internal Server Error","digest":"..."}; the message and stack are logged under the digest, never sent.
  • A route.ts that throws while it is imported answers that 500 for every method, OPTIONS included, and keeps its URL: no sibling page or 404 takes over. In development the response carries the import error.
  • Never cached. Handler responses are not stored or shared between requests, and get no default Cache-Control (an event stream gets no-cache, a redirect() gets private, no-cache unless its headers set one). Set one on the Response for browsers and CDNs.
  • Checks before the handler. A cross-site POST, PUT, PATCH or DELETE gets 403 from the CSRF check, and a body over [server] max_body_bytes gets 413, before your code runs.

Types

RouteHandler (RouteHandler<Route>) types a whole handler, and GioRequest (GioRequest<Route>) the request alone. Route is a pattern of your app ('/api/items/:id') or a params shape ({ id: string }).

Examples

Create from JSON

app/api/notes/route.ts
import type { GioRequest } from '@gio.js/core';
import { db } from '../../../lib/db.server.ts';

export async function POST(req: GioRequest) {
  const { text } = req.json<{ text: string }>();      // 415 unless sent as JSON
  const note = await db.notes.insert({ text });
  return Response.json(note, { status: 201, headers: { location: `/api/notes/${note.id}` } });
}

Redirect from a handler

Return (or throw) redirect(). A relative URL is sent as written:

app/api/go/route.ts
import { redirect } from '@gio.js/core';

export function POST() {
  return redirect('/thanks');                     // 303, Location: /thanks
}

Server-Sent Events

app/ticker/route.ts
import { GioEventStream } from '@gio.js/core';

export function GET() {
  return new GioEventStream((stream) => {
    const timer = setInterval(() => stream.send({ time: Date.now() }), 1000);
    return () => clearInterval(timer);            // runs when the client disconnects
  });
}

Set several cookies

app/api/prefs/route.ts
import { serializeCookie } from '@gio.js/core';

export function POST() {
  const headers = new Headers();
  headers.append('set-cookie', serializeCookie('theme', 'dark', { httpOnly: false }));
  headers.append('set-cookie', serializeCookie('lang', 'en', { httpOnly: false }));
  return new Response(null, { status: 204, headers });
}

Good to know

  • Response.redirect('/path') throws a TypeError (a 500): the web standard accepts only an absolute URL there. Use redirect(), or a Response with a location header.
  • A redirect answering a <GioForm> submission (a 301/302/303 to a request with x-gio-form: 1) becomes a 204 with x-gio-redirect, as it does for page actions.
  • Exporting wsHandler from the same file adds a WebSocket endpoint at the same path - see wsHandler.
  • Under gio export route handlers are skipped: a static host has no server to run them.
  • CORS is yours to answer: there is no OPTIONS export, so a preflight gets 405. Add the CORS headers to your responses (or a [[headers]] rule) for simple requests.

Version history

VersionChanges
v0.1.0-beta.8notFound() answers a JSON 404; req.formData(), req.ip, req.scheme, req.host and req.requestId; json() requires a JSON content type (415); every Set-Cookie of a Response is sent; ReadableStream bodies stream; redirect(), returned or thrown, redirects; a json() body that does not parse is a 400 (MalformedBodyError) instead of a 500; a route.ts that throws while it is imported answers 500; typed with RouteHandler.
v0.1.0-beta.5Introduced: method handlers returning a Response, a GioEventStream, null (204) or JSON, with 405 and Allow for other methods.