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>:
| Field | Type | Default | Description |
|---|---|---|---|
method | string | - | The request method. When the GET handler answers a HEAD request, it is 'HEAD'. |
path | string | - | The routed path, without the query string. |
params | ParamsOf<Route> | - | The dynamic segments. A catch-all is one /-joined string. |
query | Record<string, string> | - | The query string, one value per name: the last one when a name repeats. |
headers | Record<string, string> | - | The request headers, names lowercase. |
cookies | Record<string, string> | - | The Cookie header, parsed. |
body | string | null | - | The raw body: UTF-8 text, or base64 when bodyBase64 is true. null without one. |
bodyBase64 | boolean | - | 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). |
locale | string | undefined | - | The request locale, with [i18n]. |
ip | string | undefined | - | The client's address; behind a proxy only when it is in [server] trusted_proxies. Never read x-forwarded-for yourself. |
scheme, host | string | 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. |
requestId | string | undefined | - | The response's X-Request-Id, also on every log line. |
Returns
| Return value | Response |
|---|---|
A web Response | Its 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 GioEventStream | Server-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 thrown | Its status (303 by default), its headers and Location with the URL as written, an empty body. |
null or undefined | 204 No Content. |
| Any other value | 200, the value as JSON (application/json; charset=utf-8). |
Behavior
- HEAD is answered by the
GEThandler, without the body.HEADandOPTIONScannot be exported: only the five methods above are read. - Other methods answer
405with{"error":"Method Not Allowed"}and anAllowheader listing what the file exports (plusHEADwhen it exportsGET).OPTIONSis answered the same way. - A page in the same folder serves the methods the
route.tsdoes not export:GET/HEADrender the page, andPOSTruns its action. A405from that folder lists both files' methods. - Errors.
notFound()answers404{"error":"Not Found"}. A body error fromjson()orformData()answers415or400(see request body errors) and is not logged. Any other thrown error answers500{"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
500for every method,OPTIONSincluded, 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 getsno-cache, aredirect()getsprivate, no-cacheunless its headers set one). Set one on theResponsefor browsers and CDNs. - Checks before the handler. A cross-site
POST,PUT,PATCHorDELETEgets403from the CSRF check, and a body over[server] max_body_bytesgets413, 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 aTypeError(a500): the web standard accepts only an absolute URL there. Useredirect(), or aResponsewith alocationheader.- A redirect answering a
<GioForm>submission (a301/302/303to a request withx-gio-form: 1) becomes a204withx-gio-redirect, as it does for page actions. - Exporting
wsHandlerfrom the same file adds a WebSocket endpoint at the same path - seewsHandler. - Under
gio exportroute handlers are skipped: a static host has no server to run them. - CORS is yours to answer: there is no
OPTIONSexport, so a preflight gets405. Add the CORS headers to your responses (or a[[headers]]rule) for simple requests.
Related
- Route Handlers - the guide, with streaming responses
route.tsGioEventStream, cookies, request body errorswsHandler[security.csrf]
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | notFound() 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.5 | Introduced: method handlers returning a Response, a GioEventStream, null (204) or JSON, with 405 and Allow for other methods. |